digital watch plant is now also a statechart
This commit is contained in:
parent
3e5dca437b
commit
e27d3c4c88
12 changed files with 334 additions and 118 deletions
|
|
@ -75,6 +75,9 @@ details > summary:hover {
|
||||||
color: black;
|
color: black;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
.inputEvent:disabled {
|
||||||
|
color: darkgrey;
|
||||||
|
}
|
||||||
.inputEvent * {
|
.inputEvent * {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
124
src/App/App.tsx
124
src/App/App.tsx
|
|
@ -21,6 +21,9 @@ import { ShowAST, ShowInputEvents, ShowInternalEvents, ShowOutputEvents } from "
|
||||||
import { TopPanel } from "./TopPanel/TopPanel";
|
import { TopPanel } from "./TopPanel/TopPanel";
|
||||||
import { getKeyHandler } from "./VisualEditor/shortcut_handler";
|
import { getKeyHandler } from "./VisualEditor/shortcut_handler";
|
||||||
import { InsertMode, VisualEditor, VisualEditorState } from "./VisualEditor/VisualEditor";
|
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 = {
|
export type EditHistory = {
|
||||||
current: VisualEditorState,
|
current: VisualEditorState,
|
||||||
|
|
@ -31,8 +34,7 @@ export type EditHistory = {
|
||||||
const plants: [string, Plant<any>][] = [
|
const plants: [string, Plant<any>][] = [
|
||||||
["dummy", DummyPlant],
|
["dummy", DummyPlant],
|
||||||
["microwave", MicrowavePlant],
|
["microwave", MicrowavePlant],
|
||||||
|
["digital watch", DigitalWatchPlant],
|
||||||
// ["digital watch", DigitalWatchPlant],
|
|
||||||
]
|
]
|
||||||
|
|
||||||
export type TraceItemError = {
|
export type TraceItemError = {
|
||||||
|
|
@ -198,6 +200,121 @@ export function App() {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, [setEditHistory]);
|
}, [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(() => {
|
const scrollDownSidebar = useCallback(() => {
|
||||||
if (refRightSideBar.current) {
|
if (refRightSideBar.current) {
|
||||||
|
|
@ -400,7 +517,7 @@ export function App() {
|
||||||
style={{flex: '0 0 content'}}
|
style={{flex: '0 0 content'}}
|
||||||
>
|
>
|
||||||
{editHistory && <TopPanel
|
{editHistory && <TopPanel
|
||||||
{...{trace, time, setTime, onUndo, onRedo, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}}
|
{...{trace, time, setTime, onUndo, onRedo, onRotate, onInit, onClear, onBack, insertMode, setInsertMode, setModal, zoom, setZoom, showKeys, setShowKeys, editHistory}}
|
||||||
/>}
|
/>}
|
||||||
</div>
|
</div>
|
||||||
{/* Editor */}
|
{/* Editor */}
|
||||||
|
|
@ -458,7 +575,6 @@ export function App() {
|
||||||
{plantConns && <ShowConns {...plantConns} />}
|
{plantConns && <ShowConns {...plantConns} />}
|
||||||
{currentBigStep && <plant.render state={currentBigStep.state.plant} speed={speed}
|
{currentBigStep && <plant.render state={currentBigStep.state.plant} speed={speed}
|
||||||
raiseInput={e => onRaise("PLANT_UI_"+e.name, e.param)}
|
raiseInput={e => onRaise("PLANT_UI_"+e.name, e.param)}
|
||||||
raiseOutput={() => {}}
|
|
||||||
/>}
|
/>}
|
||||||
</PersistentDetails>
|
</PersistentDetails>
|
||||||
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary>
|
<details open={showExecutionTrace} onToggle={e => setShowExecutionTrace(e.newState === "open")}><summary>execution trace</summary>
|
||||||
|
|
|
||||||
|
|
@ -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 imgNote from "./noteSmall.png";
|
||||||
import imgWatch from "./watch.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";
|
export const [dwatchAbstractSyntax, dwatchErrors] = parseStatechart(dwatchConcreteSyntax as ConcreteSyntax, detectConnections(dwatchConcreteSyntax as ConcreteSyntax));
|
||||||
import "./DigitalWatch.css";
|
|
||||||
import { useAudioContext } from "@/App/useAudioContext";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
|
|
||||||
type DigitalWatchState = {
|
if (dwatchErrors.length > 0) {
|
||||||
light: boolean;
|
console.log({dwatchErrors});
|
||||||
h: number;
|
throw new Error("there were errors parsing dwatch plant model. see console.")
|
||||||
m: number;
|
|
||||||
s: number;
|
|
||||||
alarm: boolean;
|
|
||||||
beep: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => ("0"+n.toString()).slice(-2);
|
||||||
const twoDigits = (n: number) => n < 0 ? " " : ("0"+n.toString()).slice(-2);
|
|
||||||
const hhmmss = `${twoDigits(h)}:${twoDigits(m)}:${twoDigits(s)}`;
|
export function DigitalWatch({state, speed, raiseInput}: PlantRenderProps<RT_Statechart>) {
|
||||||
|
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);
|
const [playSound, preloadAudio] = useAudioContext(speed);
|
||||||
|
|
||||||
preloadAudio(sndBeep);
|
preloadAudio(sndBeep);
|
||||||
|
|
||||||
|
const beep = state.mode.has("270");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (beep) {
|
if (beep) {
|
||||||
playSound(sndBeep, false);
|
playSound(sndBeep, false);
|
||||||
|
|
@ -57,26 +82,26 @@ export function DigitalWatch({state: {light, h, m, s, alarm, beep}, speed, callb
|
||||||
<svg version="1.1" width="222" height="236" style={{userSelect: 'none'}}>
|
<svg version="1.1" width="222" height="236" style={{userSelect: 'none'}}>
|
||||||
<image width="222" height="236" xlinkHref={imgWatch}/>
|
<image width="222" height="236" xlinkHref={imgWatch}/>
|
||||||
|
|
||||||
{light &&
|
{lightOn &&
|
||||||
<rect x={52} y={98} width={120} height={52} fill="#deeaffff" rx={5} ry={5} />}
|
<rect x={52} y={98} width={120} height={52} fill="#deeaffff" rx={5} ry={5} />}
|
||||||
|
|
||||||
<text x="111" y="126" dominantBaseline="middle" textAnchor="middle" fontFamily="digital-font" fontSize={28} style={{whiteSpace:'preserve'}}>{hhmmss}</text>
|
<text x="111" y="126" dominantBaseline="middle" textAnchor="middle" fontFamily="digital-font" fontSize={28} style={{whiteSpace:'preserve'}}>{hhmmss}</text>
|
||||||
|
|
||||||
<rect className="watchButtonHelper" x={0} y={54} width={24} height={24}
|
<rect className="watchButtonHelper" x={0} y={54} width={24} height={24}
|
||||||
onMouseDown={() => callbacks.onTopLeftPressed()}
|
onMouseDown={() => raiseInput({name: "topLeftPressed"})}
|
||||||
onMouseUp={() => callbacks.onTopLeftReleased()}
|
onMouseUp={() => raiseInput({name: "topLeftReleased"})}
|
||||||
/>
|
/>
|
||||||
<rect className="watchButtonHelper" x={198} y={54} width={24} height={24}
|
<rect className="watchButtonHelper" x={198} y={54} width={24} height={24}
|
||||||
onMouseDown={() => callbacks.onTopRightPressed()}
|
onMouseDown={() => raiseInput({name: "topRightPressed"})}
|
||||||
onMouseUp={() => callbacks.onTopRightReleased()}
|
onMouseUp={() => raiseInput({name: "topRightReleased"})}
|
||||||
/>
|
/>
|
||||||
<rect className="watchButtonHelper" x={0} y={154} width={24} height={24}
|
<rect className="watchButtonHelper" x={0} y={154} width={24} height={24}
|
||||||
onMouseDown={() => callbacks.onBottomLeftPressed()}
|
onMouseDown={() => raiseInput({name: "bottomLeftPressed"})}
|
||||||
onMouseUp={() => callbacks.onBottomLeftReleased()}
|
onMouseUp={() => raiseInput({name: "bottomLeftReleased"})}
|
||||||
/>
|
/>
|
||||||
<rect className="watchButtonHelper" x={198} y={154} width={24} height={24}
|
<rect className="watchButtonHelper" x={198} y={154} width={24} height={24}
|
||||||
onMouseDown={() => callbacks.onBottomRightPressed()}
|
onMouseDown={() => raiseInput({name: "bottomRightPressed"})}
|
||||||
onMouseUp={() => callbacks.onBottomRightReleased()}
|
onMouseUp={() => raiseInput({name: "bottomRightReleased"})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{alarm &&
|
{alarm &&
|
||||||
|
|
@ -86,16 +111,25 @@ export function DigitalWatch({state: {light, h, m, s, alarm, beep}, speed, callb
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DigitalWatchPlant: Plant<DigitalWatchState> = {
|
export const DigitalWatchPlant: Plant<BigStep> = {
|
||||||
inputEvents: [
|
inputEvents: [
|
||||||
{ kind: "event", event: "setH", paramName: 'h' },
|
{ kind: "event", event: "displayTime" },
|
||||||
{ kind: "event", event: "setM", paramName: 'm' },
|
{ kind: "event", event: "displayChrono" },
|
||||||
{ kind: "event", event: "setS", paramName: 's' },
|
{ kind: "event", event: "displayAlarm" },
|
||||||
{ kind: "event", event: "setLight", paramName: 'lightOn'},
|
{ 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: "setAlarm", paramName: 'alarmOn'},
|
||||||
{ kind: "event", event: "beep", paramName: 'beep'},
|
{ kind: "event", event: "beep", paramName: 'beep'},
|
||||||
],
|
|
||||||
outputEvents: [
|
// UI events
|
||||||
{ kind: "event", event: "topLeftPressed" },
|
{ kind: "event", event: "topLeftPressed" },
|
||||||
{ kind: "event", event: "topRightPressed" },
|
{ kind: "event", event: "topRightPressed" },
|
||||||
{ kind: "event", event: "bottomRightPressed" },
|
{ kind: "event", event: "bottomRightPressed" },
|
||||||
|
|
@ -105,49 +139,18 @@ export const DigitalWatchPlant: Plant<DigitalWatchState> = {
|
||||||
{ kind: "event", event: "bottomRightReleased" },
|
{ kind: "event", event: "bottomRightReleased" },
|
||||||
{ kind: "event", event: "bottomLeftReleased" },
|
{ kind: "event", event: "bottomLeftReleased" },
|
||||||
],
|
],
|
||||||
initial: {
|
outputEvents: [
|
||||||
light: false,
|
{ kind: "event", event: "alarm" },
|
||||||
alarm: false,
|
|
||||||
h: 12,
|
{ kind: "event", event: "topLeftPressed" },
|
||||||
m: 0,
|
{ kind: "event", event: "topRightPressed" },
|
||||||
s: 0,
|
{ kind: "event", event: "bottomRightPressed" },
|
||||||
beep: false,
|
{ kind: "event", event: "bottomLeftPressed" },
|
||||||
},
|
{ kind: "event", event: "topLeftReleased" },
|
||||||
reduce: (inputEvent: RaisedEvent, state: DigitalWatchState) => {
|
{ kind: "event", event: "topRightReleased" },
|
||||||
if (inputEvent.name === "setH") {
|
{ kind: "event", event: "bottomRightReleased" },
|
||||||
return { ...state, h: inputEvent.param };
|
{ kind: "event", event: "bottomLeftReleased" },
|
||||||
}
|
],
|
||||||
if (inputEvent.name === "setM") {
|
execution: statechartExecution(dwatchAbstractSyntax),
|
||||||
return { ...state, m: inputEvent.param };
|
render: DigitalWatch,
|
||||||
}
|
|
||||||
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) => <DigitalWatch state={state} speed={speed} callbacks={{
|
|
||||||
onTopLeftPressed: () => 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"}),
|
|
||||||
}}/>,
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
1
src/App/Plant/DigitalWatch/model.json
Normal file
1
src/App/Plant/DigitalWatch/model.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -13,16 +13,10 @@ import { useEffect } from "react";
|
||||||
|
|
||||||
import "./Microwave.css";
|
import "./Microwave.css";
|
||||||
import { useAudioContext } from "../../useAudioContext";
|
import { useAudioContext } from "../../useAudioContext";
|
||||||
import { Plant } from "../Plant";
|
import { Plant, PlantRenderProps } from "../Plant";
|
||||||
import { statechartExecution } from "@/statecharts/timed_reactive";
|
import { statechartExecution } from "@/statecharts/timed_reactive";
|
||||||
import { microwaveAbstractSyntax } from "./model";
|
import { microwaveAbstractSyntax } from "./model";
|
||||||
|
|
||||||
export type MicrowaveProps = {
|
|
||||||
state: RT_Statechart,
|
|
||||||
speed: number,
|
|
||||||
raiseInput: (event: RaisedEvent) => void;
|
|
||||||
raiseOutput: (event: RaisedEvent) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const imgs = {
|
const imgs = {
|
||||||
"false": { "false": imgSmallClosedOff, "true": imgSmallClosedOn },
|
"false": { "false": imgSmallClosedOff, "true": imgSmallClosedOn },
|
||||||
|
|
@ -43,7 +37,7 @@ const DOOR_Y0 = 68;
|
||||||
const DOOR_WIDTH = 353;
|
const DOOR_WIDTH = 353;
|
||||||
const DOOR_HEIGHT = 217;
|
const DOOR_HEIGHT = 217;
|
||||||
|
|
||||||
export function Magnetron({state, speed, raiseInput, raiseOutput}: MicrowaveProps) {
|
export function Microwave({state, speed, raiseInput}: PlantRenderProps<RT_Statechart>) {
|
||||||
const [playSound, preloadAudio] = useAudioContext(speed);
|
const [playSound, preloadAudio] = useAudioContext(speed);
|
||||||
|
|
||||||
// preload(imgSmallClosedOff, {as: "image"});
|
// preload(imgSmallClosedOff, {as: "image"});
|
||||||
|
|
@ -134,5 +128,5 @@ export const MicrowavePlant: Plant<BigStep> = {
|
||||||
{kind: "event", event: "incTimeReleased"},
|
{kind: "event", event: "incTimeReleased"},
|
||||||
],
|
],
|
||||||
execution: statechartExecution(microwaveAbstractSyntax),
|
execution: statechartExecution(microwaveAbstractSyntax),
|
||||||
render: Magnetron,
|
render: Microwave,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ export type PlantRenderProps<StateType> = {
|
||||||
state: StateType,
|
state: StateType,
|
||||||
speed: number,
|
speed: number,
|
||||||
raiseInput: (e: RaisedEvent) => void,
|
raiseInput: (e: RaisedEvent) => void,
|
||||||
raiseOutput: (e: RaisedEvent) => void,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Plant<StateType> = {
|
export type Plant<StateType> = {
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ export const RTHistoryItem = memo(function RTHistoryItem({ast, idx, item, prevIt
|
||||||
<div>
|
<div>
|
||||||
{formatTime(item.simtime)}
|
{formatTime(item.simtime)}
|
||||||
 
|
 
|
||||||
<div className="inputEvent"><RTCause cause={item.state.sc.inputEvent}/></div>
|
<div className="inputEvent"><RTCause cause={isPlantStep ? item.state.plant.inputEvent : item.state.sc.inputEvent}/></div>
|
||||||
</div>
|
</div>
|
||||||
<ShowMode mode={newStates} statechart={ast}/>
|
<ShowMode mode={newStates} statechart={ast}/>
|
||||||
<ShowEnvironment environment={item.state.sc.environment}/>
|
<ShowEnvironment environment={item.state.sc.environment}/>
|
||||||
|
|
|
||||||
23
src/App/TopPanel/RotateButtons.tsx
Normal file
23
src/App/TopPanel/RotateButtons.tsx
Normal file
|
|
@ -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 <>
|
||||||
|
<button
|
||||||
|
title="rotate selection 90 degrees counter-clockwise"
|
||||||
|
onClick={() => onRotate("ccw")}
|
||||||
|
disabled={disabled}>
|
||||||
|
{<Rotate90DegreesCcwTwoToneIcon fontSize="small"/>}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
title="rotate selection 90 degrees clockwise"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => onRotate("cw")}>
|
||||||
|
{<Rotate90DegreesCwTwoToneIcon fontSize="small"/>}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
});
|
||||||
|
|
@ -19,6 +19,7 @@ import SkipNextIcon from '@mui/icons-material/SkipNext';
|
||||||
import StopIcon from '@mui/icons-material/Stop';
|
import StopIcon from '@mui/icons-material/Stop';
|
||||||
import { InsertModes } from "./InsertModes";
|
import { InsertModes } from "./InsertModes";
|
||||||
import { usePersistentState } from "@/App/persistent_state";
|
import { usePersistentState } from "@/App/persistent_state";
|
||||||
|
import { RotateButtons } from "./RotateButtons";
|
||||||
|
|
||||||
export type TopPanelProps = {
|
export type TopPanelProps = {
|
||||||
trace: TraceState | null,
|
trace: TraceState | null,
|
||||||
|
|
@ -26,6 +27,7 @@ export type TopPanelProps = {
|
||||||
setTime: Dispatch<SetStateAction<TimeMode>>,
|
setTime: Dispatch<SetStateAction<TimeMode>>,
|
||||||
onUndo: () => void,
|
onUndo: () => void,
|
||||||
onRedo: () => void,
|
onRedo: () => void,
|
||||||
|
onRotate: (direction: "ccw"|"cw") => void,
|
||||||
onInit: () => void,
|
onInit: () => void,
|
||||||
onClear: () => void,
|
onClear: () => void,
|
||||||
onBack: () => void,
|
onBack: () => void,
|
||||||
|
|
@ -41,7 +43,7 @@ export type TopPanelProps = {
|
||||||
|
|
||||||
const ShortCutShowKeys = <kbd>~</kbd>;
|
const ShortCutShowKeys = <kbd>~</kbd>;
|
||||||
|
|
||||||
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 [displayTime, setDisplayTime] = useState("0.000");
|
||||||
const [timescale, setTimescale] = usePersistentState("timescale", 1);
|
const [timescale, setTimescale] = usePersistentState("timescale", 1);
|
||||||
|
|
||||||
|
|
@ -212,6 +214,11 @@ export const TopPanel = memo(function TopPanel({trace, time, setTime, onUndo, on
|
||||||
 
|
 
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="toolbarGroup">
|
||||||
|
<RotateButtons selection={editHistory.current.selection} onRotate={onRotate}/>
|
||||||
|
 
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* execution */}
|
{/* execution */}
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,11 @@ type HistorySelectable = {
|
||||||
}
|
}
|
||||||
type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable | HistorySelectable;
|
type Selectable = RountangleSelectable | ArrowSelectable | TextSelectable | HistorySelectable;
|
||||||
|
|
||||||
type Selection = Selectable[];
|
export type Selection = Selectable[];
|
||||||
|
|
||||||
export type InsertMode = "and"|"or"|"pseudo"|"shallow"|"deep"|"transition"|"text";
|
export type InsertMode = "and"|"or"|"pseudo"|"shallow"|"deep"|"transition"|"text";
|
||||||
|
|
||||||
|
|
||||||
type VisualEditorProps = {
|
type VisualEditorProps = {
|
||||||
state: VisualEditorState,
|
state: VisualEditorState,
|
||||||
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
setState: Dispatch<(v:VisualEditorState) => VisualEditorState>,
|
||||||
|
|
@ -75,6 +76,8 @@ export const VisualEditor = memo(function VisualEditor({state, setState, trace,
|
||||||
|
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
|
|
||||||
|
window.setState = setState;
|
||||||
|
|
||||||
// uid's of selected rountangles
|
// uid's of selected rountangles
|
||||||
const selection = state.selection || [];
|
const selection = state.selection || [];
|
||||||
const setSelection = useCallback((cb: (oldSelection: Selection) => Selection) =>
|
const setSelection = useCallback((cb: (oldSelection: Selection) => Selection) =>
|
||||||
|
|
|
||||||
|
|
@ -175,15 +175,20 @@ export function coupledExecution<T extends {[name: string]: any}>(models: {[name
|
||||||
throw new Error("cannot make intTransition - timeAdvance is infinity");
|
throw new Error("cannot make intTransition - timeAdvance is infinity");
|
||||||
},
|
},
|
||||||
extTransition: (simtime, c, e) => {
|
extTransition: (simtime, c, e) => {
|
||||||
console.log(e);
|
if (!Object.hasOwn(conns.inputEvents, e.name)) {
|
||||||
const {model, eventName} = conns.inputEvents[e.name];
|
console.warn('input event', e.name, 'goes to nowhere');
|
||||||
console.log('input event', e.name, 'goes to', `${model}.${eventName}`);
|
return [[], c];
|
||||||
const inputEvent: InputEvent = {
|
}
|
||||||
kind: "input",
|
else {
|
||||||
name: eventName,
|
const {model, eventName} = conns.inputEvents[e.name];
|
||||||
param: e.param,
|
console.log('input event', e.name, 'goes to', `${model}.${eventName}`);
|
||||||
};
|
const inputEvent: InputEvent = {
|
||||||
return makeModelExtTransition(simtime, c, model, inputEvent);
|
kind: "input",
|
||||||
|
name: eventName,
|
||||||
|
param: e.param,
|
||||||
|
};
|
||||||
|
return makeModelExtTransition(simtime, c, model, inputEvent);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,3 +203,65 @@ export const sides: [RectSide, (r: Rect2D) => Line2D][] = [
|
||||||
["right", getRightSide],
|
["right", getRightSide],
|
||||||
["bottom", getBottomSide],
|
["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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue