implement zoom
This commit is contained in:
parent
523e00d5dc
commit
6bbb230636
5 changed files with 54 additions and 14 deletions
|
|
@ -45,6 +45,7 @@ export function App() {
|
||||||
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
|
const [time, setTime] = useState<TimeMode>({kind: "paused", simtime: 0});
|
||||||
const [modal, setModal] = useState<ReactElement|null>(null);
|
const [modal, setModal] = useState<ReactElement|null>(null);
|
||||||
const [plantName, setPlantName] = usePersistentState("plant", "dummy");
|
const [plantName, setPlantName] = usePersistentState("plant", "dummy");
|
||||||
|
const [zoom, setZoom] = usePersistentState("zoom", 1);
|
||||||
|
|
||||||
const plant = plants.find(([pn, p]) => pn === plantName)![1];
|
const plant = plants.find(([pn, p]) => pn === plantName)![1];
|
||||||
|
|
||||||
|
|
@ -248,12 +249,12 @@ export function App() {
|
||||||
>
|
>
|
||||||
<TopPanel
|
<TopPanel
|
||||||
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
|
rt={rtIdx === undefined ? undefined : rt[rtIdx]}
|
||||||
{...{rtIdx, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal}}
|
{...{rtIdx, ast, time, setTime, onUndo, onRedo, onInit, onClear, onRaise, onBack, mode, setMode, setModal, zoom, setZoom}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{/* Below the top bar: Editor */}
|
{/* Below the top bar: Editor */}
|
||||||
<Box sx={{flexGrow:1, overflow: "auto"}}>
|
<Box sx={{flexGrow:1, overflow: "auto"}}>
|
||||||
<VisualEditor {...{state: editorState, setState: setEditorState, ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint}}/>
|
<VisualEditor {...{state: editorState, setState: setEditorState, ast, setAST, rt: rt.at(rtIdx!), setRT, errors, setErrors, mode, highlightActive, highlightTransitions, setModal, makeCheckPoint, zoom}}/>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ import UndoIcon from '@mui/icons-material/Undo';
|
||||||
import RedoIcon from '@mui/icons-material/Redo';
|
import RedoIcon from '@mui/icons-material/Redo';
|
||||||
import InfoOutlineIcon from '@mui/icons-material/InfoOutline';
|
import InfoOutlineIcon from '@mui/icons-material/InfoOutline';
|
||||||
import KeyboardIcon from '@mui/icons-material/Keyboard';
|
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 { formatTime } from "./util";
|
||||||
import { InsertMode } from "../VisualEditor/VisualEditor";
|
import { InsertMode } from "../VisualEditor/VisualEditor";
|
||||||
|
|
@ -38,9 +40,11 @@ export type TopPanelProps = {
|
||||||
mode: InsertMode,
|
mode: InsertMode,
|
||||||
setMode: Dispatch<SetStateAction<InsertMode>>,
|
setMode: Dispatch<SetStateAction<InsertMode>>,
|
||||||
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
||||||
|
zoom: number,
|
||||||
|
setZoom: Dispatch<SetStateAction<number>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 [displayTime, setDisplayTime] = useState("0.000");
|
||||||
const [timescale, setTimescale] = useState(1);
|
const [timescale, setTimescale] = useState(1);
|
||||||
const [showKeys, setShowKeys] = usePersistentState("shortcuts", true);
|
const [showKeys, setShowKeys] = usePersistentState("shortcuts", true);
|
||||||
|
|
@ -99,6 +103,14 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onRedo();
|
onRedo();
|
||||||
}
|
}
|
||||||
|
if (e.key === "+") {
|
||||||
|
e.preventDefault();
|
||||||
|
onZoomIn();
|
||||||
|
}
|
||||||
|
if (e.key === "-") {
|
||||||
|
e.preventDefault();
|
||||||
|
onZoomOut();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("keydown", onKeyDown);
|
window.addEventListener("keydown", onKeyDown);
|
||||||
|
|
@ -123,6 +135,13 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
}
|
}
|
||||||
}, [time]);
|
}, [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) {
|
function onChangePaused(paused: boolean, wallclktime: number) {
|
||||||
setTime(time => {
|
setTime(time => {
|
||||||
if (paused) {
|
if (paused) {
|
||||||
|
|
@ -189,6 +208,16 @@ export function TopPanel({rt, rtIdx, time, setTime, onUndo, onRedo, onInit, onCl
|
||||||
 
|
 
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* zoom */}
|
||||||
|
<div className="toolbarGroup">
|
||||||
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>+</kbd></>}>
|
||||||
|
<button title="zoom in" onClick={onZoomIn}><ZoomInIcon fontSize="small"/></button>
|
||||||
|
</KeyInfo>
|
||||||
|
<KeyInfo keyInfo={<><kbd>Ctrl</kbd>+<kbd>-</kbd></>}>
|
||||||
|
<button title="zoom out" onClick={onZoomOut}><ZoomOutIcon fontSize="small"/></button>
|
||||||
|
</KeyInfo>
|
||||||
|
 
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* undo / redo */}
|
{/* undo / redo */}
|
||||||
<div className="toolbarGroup">
|
<div className="toolbarGroup">
|
||||||
|
|
|
||||||
|
|
@ -45,19 +45,19 @@ export function DigitalWatch({light, h, m, s, alarm, callbacks}: DigitalWatchPro
|
||||||
|
|
||||||
<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 x="0" y="59" width="16" height="16" fill="#fff" fill-opacity="0"
|
<rect x="0" y="59" width="16" height="16" fill="#fff" fillOpacity="0"
|
||||||
onMouseDown={() => callbacks.onTopLeftPressed()}
|
onMouseDown={() => callbacks.onTopLeftPressed()}
|
||||||
onMouseUp={() => callbacks.onTopLeftReleased()}
|
onMouseUp={() => callbacks.onTopLeftReleased()}
|
||||||
/>
|
/>
|
||||||
<rect x="206" y="57" width="16" height="16" fill="#fff" fill-opacity="0"
|
<rect x="206" y="57" width="16" height="16" fill="#fff" fillOpacity="0"
|
||||||
onMouseDown={() => callbacks.onTopRightPressed()}
|
onMouseDown={() => callbacks.onTopRightPressed()}
|
||||||
onMouseUp={() => callbacks.onTopRightReleased()}
|
onMouseUp={() => callbacks.onTopRightReleased()}
|
||||||
/>
|
/>
|
||||||
<rect x="0" y="158" width="16" height="16" fill="#fff" fill-opacity="0"
|
<rect x="0" y="158" width="16" height="16" fill="#fff" fillOpacity="0"
|
||||||
onMouseDown={() => callbacks.onBottomLeftPressed()}
|
onMouseDown={() => callbacks.onBottomLeftPressed()}
|
||||||
onMouseUp={() => callbacks.onBottomLeftReleased()}
|
onMouseUp={() => callbacks.onBottomLeftReleased()}
|
||||||
/>
|
/>
|
||||||
<rect x="208" y="158" width="16" height="16" fill="#fff" fill-opacity="0"
|
<rect x="208" y="158" width="16" height="16" fill="#fff" fillOpacity="0"
|
||||||
onMouseDown={() => callbacks.onBottomRightPressed()}
|
onMouseDown={() => callbacks.onBottomRightPressed()}
|
||||||
onMouseUp={() => callbacks.onBottomRightReleased()}
|
onMouseUp={() => callbacks.onBottomRightReleased()}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { Statechart } from "../statecharts/abstract_syntax";
|
||||||
import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text } from "../statecharts/concrete_syntax";
|
import { Arrow, ArrowPart, Diamond, History, Rountangle, RountanglePart, Text } from "../statecharts/concrete_syntax";
|
||||||
import { parseStatechart, TraceableError } from "../statecharts/parser";
|
import { parseStatechart, TraceableError } from "../statecharts/parser";
|
||||||
import { BigStep } from "../statecharts/runtime_types";
|
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 { MIN_ROUNTANGLE_SIZE } from "./parameters";
|
||||||
import { getBBoxInSvgCoords } from "./svg_helper";
|
import { getBBoxInSvgCoords } from "./svg_helper";
|
||||||
import { ArrowSVG } from "./ArrowSVG";
|
import { ArrowSVG } from "./ArrowSVG";
|
||||||
|
|
@ -73,9 +73,10 @@ type VisualEditorProps = {
|
||||||
highlightTransitions: string[],
|
highlightTransitions: string[],
|
||||||
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
setModal: Dispatch<SetStateAction<ReactElement|null>>,
|
||||||
makeCheckPoint: () => void;
|
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);
|
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}) {
|
function getCurrentPointer(e: {pageX: number, pageY: number}) {
|
||||||
const bbox = refSVG.current!.getBoundingClientRect();
|
const bbox = refSVG.current!.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
x: e.pageX - bbox.left,
|
x: (e.pageX - bbox.left)/zoom,
|
||||||
y: e.pageY - bbox.top,
|
y: (e.pageY - bbox.top)/zoom,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,7 +294,7 @@ export function VisualEditor({state, setState, ast, setAST, rt, errors, setError
|
||||||
const currentPointer = getCurrentPointer(e);
|
const currentPointer = getCurrentPointer(e);
|
||||||
if (dragging) {
|
if (dragging) {
|
||||||
// const pointerDelta = subtractV2D(currentPointer, dragging.lastMousePos);
|
// 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 => ({
|
setState(state => ({
|
||||||
...state,
|
...state,
|
||||||
rountangles: state.rountangles.map(r => {
|
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 shapes = Array.from(refSVG.current?.querySelectorAll("rect, line, circle, text") || []) as SVGGraphicsElement[];
|
||||||
const shapesInSelection = shapes.filter(el => {
|
const shapesInSelection = shapes.filter(el => {
|
||||||
const bbox = getBBoxInSvgCoords(el, refSVG.current!);
|
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"));
|
}).filter(el => !el.classList.contains("corner"));
|
||||||
|
|
||||||
const uidToParts = new Map();
|
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);
|
const rootErrors = errors.filter(({shapeUid}) => shapeUid === "root").map(({message}) => message);
|
||||||
|
|
||||||
return <svg width="4000px" height="4000px"
|
const size = 4000*zoom;
|
||||||
|
|
||||||
|
return <svg width={size} height={size}
|
||||||
className={"svgCanvas"+(active.has("root")?" active":"")+(dragging ? " dragging" : "")}
|
className={"svgCanvas"+(active.has("root")?" active":"")+(dragging ? " dragging" : "")}
|
||||||
onMouseDown={onMouseDown}
|
onMouseDown={onMouseDown}
|
||||||
onContextMenu={e => e.preventDefault()}
|
onContextMenu={e => e.preventDefault()}
|
||||||
ref={refSVG}
|
ref={refSVG}
|
||||||
|
|
||||||
|
viewBox={`0 0 4000 4000`}
|
||||||
|
|
||||||
onCopy={onCopy}
|
onCopy={onCopy}
|
||||||
onPaste={onPaste}
|
onPaste={onPaste}
|
||||||
onCut={onCut}
|
onCut={onCut}
|
||||||
|
|
|
||||||
1
todo.txt
1
todo.txt
|
|
@ -45,6 +45,7 @@ TODO
|
||||||
- stuck in pseudo-state
|
- stuck in pseudo-state
|
||||||
- ???
|
- ???
|
||||||
don't crash and show the error
|
don't crash and show the error
|
||||||
|
- buttons to rotate selection 90 degrees
|
||||||
|
|
||||||
- experimental features:
|
- experimental features:
|
||||||
- multiverse execution history
|
- multiverse execution history
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue