diff --git a/src/App/App.tsx b/src/App/App.tsx index 00eb173..d7d119a 100644 --- a/src/App/App.tsx +++ b/src/App/App.tsx @@ -45,6 +45,7 @@ export function App() { const [time, setTime] = useState({kind: "paused", simtime: 0}); const [modal, setModal] = useState(null); const [plantName, setPlantName] = usePersistentState("plant", "dummy"); + const [zoom, setZoom] = usePersistentState("zoom", 1); const plant = plants.find(([pn, p]) => pn === plantName)![1]; @@ -248,12 +249,12 @@ export function App() { > {/* Below the top bar: Editor */} - + diff --git a/src/App/TopPanel.tsx b/src/App/TopPanel.tsx index 67f6c1e..da17fc5 100644 --- a/src/App/TopPanel.tsx +++ b/src/App/TopPanel.tsx @@ -15,6 +15,8 @@ import UndoIcon from '@mui/icons-material/Undo'; import RedoIcon from '@mui/icons-material/Redo'; import InfoOutlineIcon from '@mui/icons-material/InfoOutline'; import KeyboardIcon from '@mui/icons-material/Keyboard'; +import ZoomInIcon from '@mui/icons-material/ZoomIn'; +import ZoomOutIcon from '@mui/icons-material/ZoomOut'; import { formatTime } from "./util"; import { InsertMode } from "../VisualEditor/VisualEditor"; @@ -38,9 +40,11 @@ export type TopPanelProps = { mode: InsertMode, setMode: Dispatch>, setModal: Dispatch>, + zoom: number, + setZoom: Dispatch>, } -export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, ast, mode, setMode, setModal}: TopPanelProps) { +export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, ast, mode, setMode, setModal, zoom, setZoom}: TopPanelProps) { const [displayTime, setDisplayTime] = useState("0.000"); const [timescale, setTimescale] = useState(1); const [showKeys, setShowKeys] = usePersistentState("shortcuts", true); @@ -99,6 +103,14 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl e.preventDefault(); onRedo(); } + if (e.key === "+") { + e.preventDefault(); + onZoomIn(); + } + if (e.key === "-") { + e.preventDefault(); + onZoomOut(); + } } }; window.addEventListener("keydown", onKeyDown); @@ -123,6 +135,13 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl } }, [time]); + function onZoomIn() { + setZoom(zoom => Math.min(zoom * 1.25, 4)); + } + function onZoomOut() { + setZoom(zoom => Math.max(zoom / 1.25, 1/4)); + } + function onChangePaused(paused: boolean, wallclktime: number) { setTime(time => { if (paused) { @@ -189,6 +208,16 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl   + {/* zoom */} +
+ Ctrl++}> + + + Ctrl+-}> + + +   +
{/* undo / redo */}
diff --git a/src/Plant/DigitalWatch/DigitalWatch.tsx b/src/Plant/DigitalWatch/DigitalWatch.tsx index 376cb14..9d41279 100644 --- a/src/Plant/DigitalWatch/DigitalWatch.tsx +++ b/src/Plant/DigitalWatch/DigitalWatch.tsx @@ -45,19 +45,19 @@ export function DigitalWatch({light, h, m, s, alarm, callbacks}: DigitalWatchPro {hhmmss} - callbacks.onTopLeftPressed()} onMouseUp={() => callbacks.onTopLeftReleased()} /> - callbacks.onTopRightPressed()} onMouseUp={() => callbacks.onTopRightReleased()} /> - callbacks.onBottomLeftPressed()} onMouseUp={() => callbacks.onBottomLeftReleased()} /> - callbacks.onBottomRightPressed()} onMouseUp={() => callbacks.onBottomRightReleased()} /> diff --git a/src/VisualEditor/VisualEditor.tsx b/src/VisualEditor/VisualEditor.tsx index 392a4a8..d76fec9 100644 --- a/src/VisualEditor/VisualEditor.tsx +++ b/src/VisualEditor/VisualEditor.tsx @@ -4,7 +4,7 @@ import { Statechart } from "../statecharts/abstract_syntax"; import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text } from "../statecharts/concrete_syntax"; import { parseStatechart, TraceableError } from "../statecharts/parser"; import { BigStep } from "../statecharts/runtime_types"; -import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, subtractV2D, transformLine, transformRect } from "./geometry"; +import { ArcDirection, Line2D, Rect2D, Vec2D, addV2D, arcDirection, area, getBottomSide, getLeftSide, getRightSide, getTopSide, isEntirelyWithin, normalizeRect, scaleV2D, subtractV2D, transformLine, transformRect } from "./geometry"; import { MIN_ROUNTANGLE_SIZE } from "./parameters"; import { getBBoxInSvgCoords } from "./svg_helper"; import { ArrowSVG } from "./ArrowSVG"; @@ -73,9 +73,10 @@ type VisualEditorProps = { highlightTransitions: string[], setModal: Dispatch>, makeCheckPoint: () => void; + zoom: number; }; -export function VisualEditor({state, setState, ast, setAST, rt, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint}: VisualEditorProps) { +export function VisualEditor({state, setState, ast, setAST, rt, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}: VisualEditorProps) { const [dragging, setDragging] = useState(false); @@ -163,8 +164,8 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError function getCurrentPointer(e: {pageX: number, pageY: number}) { const bbox = refSVG.current!.getBoundingClientRect(); return { - x: e.pageX - bbox.left, - y: e.pageY - bbox.top, + x: (e.pageX - bbox.left)/zoom, + y: (e.pageY - bbox.top)/zoom, } } @@ -293,7 +294,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError const currentPointer = getCurrentPointer(e); if (dragging) { // const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos); - const pointerDelta = {x: e.movementX, y: e.movementY}; + const pointerDelta = {x: e.movementX/zoom, y: e.movementY/zoom}; setState(state => ({ ...state, rountangles: state.rountangles.map(r => { @@ -398,7 +399,11 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError const shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[]; const shapesInSelection = shapes.filter(el => { const bbox = getBBoxInSvgCoords(el, refSVG.current!); - return isEntirelyWithin(bbox, normalizedSS); + const scaledBBox = { + topLeft: scaleV2D(bbox.topLeft, 1/zoom), + size: scaleV2D(bbox.size, 1/zoom), + } + return isEntirelyWithin(scaledBBox, normalizedSS); }).filter(el => !el.classList.contains("corner")); const uidToParts = new Map(); @@ -666,12 +671,16 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message); - return e.preventDefault()} ref={refSVG} + viewBox={`0 0 4000 4000`} + onCopy={onCopy} onPaste={onPaste} onCut={onCut} diff --git a/todo.txt b/todo.txt index 7f519cb..b6e28bc 100644 --- a/todo.txt +++ b/todo.txt @@ -45,6 +45,7 @@ TODO - stuck in pseudo-state - ??? don't crash and show the error + - buttons to rotate selection 90 degrees - experimental features: - multiverse execution history