diff --git a/components/code-panel/code-panel.tsx b/components/code-panel/code-panel.tsx index 02a2ee6a7..2bbac4fdc 100644 --- a/components/code-panel/code-panel.tsx +++ b/components/code-panel/code-panel.tsx @@ -180,7 +180,7 @@ export default function CodePanel() { const PanelContainer = styled(motion.div, { position: "absolute", - top: "8px", + top: "48px", right: "8px", bottom: "48px", backgroundColor: "$panel", diff --git a/components/editor.tsx b/components/editor.tsx index 00f74d0b6..0793dcc56 100644 --- a/components/editor.tsx +++ b/components/editor.tsx @@ -1,6 +1,7 @@ import useKeyboardEvents from "hooks/useKeyboardEvents" import Canvas from "./canvas/canvas" import StatusBar from "./status-bar" +import Toolbar from "./toolbar" import CodePanel from "./code-panel/code-panel" export default function Editor() { @@ -10,7 +11,8 @@ export default function Editor() { <> - + + {/* */} ) } diff --git a/components/toolbar.tsx b/components/toolbar.tsx new file mode 100644 index 000000000..8d3b6e5de --- /dev/null +++ b/components/toolbar.tsx @@ -0,0 +1,128 @@ +import state, { useSelector } from "state" +import styled from "styles" +import { Menu } from "react-feather" + +export default function Toolbar() { + const activeTool = useSelector((state) => + state.whenIn({ + selecting: "select", + creatingDot: "dot", + creatingCircle: "circle", + creatingEllipse: "ellipse", + creatingRay: "ray", + creatingLine: "line", + creatingPolyline: "polyline", + creatingRectangle: "rectangle", + }) + ) + + return ( + +
+ + + + + + + + + +
+
+ ) +} + +const ToolbarContainer = styled("div", { + position: "absolute", + top: 0, + left: 0, + width: "100%", + height: 40, + userSelect: "none", + borderBottom: "1px solid black", + gridArea: "status", + display: "grid", + gridTemplateColumns: "auto 1fr auto", + alignItems: "center", + backgroundColor: "white", + gap: 8, + fontSize: "$1", + zIndex: 200, +}) + +const Section = styled("div", { + whiteSpace: "nowrap", + overflow: "hidden", + display: "flex", +}) + +const Button = styled("button", { + display: "flex", + alignItems: "center", + cursor: "pointer", + font: "$ui", + fontSize: "$ui", + height: "40px", + borderRadius: 0, + border: "none", + padding: "0 12px", + background: "none", + "&:hover": { + backgroundColor: "$hint", + }, + "& svg": { + height: 16, + width: 16, + }, + variants: { + isSelected: { + true: { + color: "$selected", + }, + false: {}, + }, + }, +}) diff --git a/hooks/useKeyboardEvents.ts b/hooks/useKeyboardEvents.ts index 115d920f0..6cd8c2d5a 100644 --- a/hooks/useKeyboardEvents.ts +++ b/hooks/useKeyboardEvents.ts @@ -1,13 +1,13 @@ import { useEffect } from "react" import state from "state" -import { getKeyboardEventInfo, isDarwin } from "utils/utils" +import { getKeyboardEventInfo, isDarwin, metaKey } from "utils/utils" export default function useKeyboardEvents() { useEffect(() => { function handleKeyDown(e: KeyboardEvent) { if (e.key === "Escape") { state.send("CANCELLED") - } else if (e.key === "z" && (isDarwin() ? e.metaKey : e.ctrlKey)) { + } else if (e.key === "z" && metaKey(e)) { if (e.shiftKey) { state.send("REDO") } else { @@ -15,7 +15,41 @@ export default function useKeyboardEvents() { } } - state.send("PRESSED_KEY", getKeyboardEventInfo(e)) + if (e.key === "Backspace" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("DELETED", getKeyboardEventInfo(e)) + } + + if (e.key === "v" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e)) + } + + if (e.key === "d" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e)) + } + + if (e.key === "c" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e)) + } + + if (e.key === "i" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e)) + } + + if (e.key === "l" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e)) + } + + if (e.key === "y" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e)) + } + + if (e.key === "p" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e)) + } + + if (e.key === "r" && !(metaKey(e) || e.shiftKey || e.altKey)) { + state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e)) + } } function handleKeyUp(e: KeyboardEvent) { diff --git a/lib/shapes/ray.tsx b/lib/shapes/ray.tsx index 3463190dc..f584f2f0c 100644 --- a/lib/shapes/ray.tsx +++ b/lib/shapes/ray.tsx @@ -24,8 +24,15 @@ const ray = createShape({ } }, - render({ id }) { - return + render({ id, direction }) { + const [x2, y2] = vec.add([0, 0], vec.mul(direction, 100000)) + + return ( + + + + + ) }, getBounds(shape) { @@ -52,7 +59,7 @@ const ray = createShape({ }, hitTest(shape, test) { - return vec.dist(shape.point, test) < 4 + return true }, hitTestBounds(this, shape, brushBounds) { diff --git a/state/data.ts b/state/data.ts index 8c2e306fc..d14d3c65b 100644 --- a/state/data.ts +++ b/state/data.ts @@ -9,6 +9,18 @@ export const defaultDocument: Data["document"] = { name: "Page 0", childIndex: 0, shapes: { + rayShape: shapeUtils[ShapeType.Ray].create({ + id: "rayShape", + name: "Ray", + childIndex: 3, + point: [300, 300], + direction: [0.5, 0.5], + style: { + fill: "#AAA", + stroke: "#777", + strokeWidth: 1, + }, + }), // shape3: shapeUtils[ShapeType.Dot].create({ // id: "shape3", // name: "Shape 3", diff --git a/state/state.ts b/state/state.ts index 71f940b33..d56a2b020 100644 --- a/state/state.ts +++ b/state/state.ts @@ -1,9 +1,17 @@ import { createSelectorHook, createState } from "@state-designer/react" import { clamp, getCommonBounds, screenToWorld } from "utils/utils" import * as vec from "utils/vec" -import { Data, PointerInfo, Shape, TransformCorner, TransformEdge } from "types" +import { + Data, + PointerInfo, + Shape, + ShapeType, + Shapes, + TransformCorner, + TransformEdge, +} from "types" import { defaultDocument } from "./data" -import { getShapeUtils } from "lib/shapes" +import shapeUtilityMap, { getShapeUtils } from "lib/shapes" import history from "state/history" import * as Sessions from "./sessions" import commands from "./commands" @@ -35,6 +43,14 @@ const state = createState({ PANNED_CAMERA: { do: "panCamera", }, + SELECTED_SELECT_TOOL: { to: "selecting" }, + SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "creatingDot" }, + SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "creatingCircle" }, + SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "creatingEllipse" }, + SELECTED_RAY_TOOL: { unless: "isReadOnly", to: "creatingRay" }, + SELECTED_LINE_TOOL: { unless: "isReadOnly", to: "creatingLine" }, + SELECTED_POLYLINE_TOOL: { unless: "isReadOnly", to: "creatingPolyline" }, + SELECTED_RECTANGLE_TOOL: { unless: "isReadOnly", to: "creatingRectangle" }, }, initial: "selecting", states: { @@ -42,6 +58,7 @@ const state = createState({ on: { UNDO: { do: "undo" }, REDO: { do: "redo" }, + DELETED: { do: "deleteSelection" }, GENERATED_SHAPES_FROM_CODE: "setGeneratedShapes", INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize", DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize", @@ -136,6 +153,37 @@ const state = createState({ }, }, }, + creatingDot: { + initial: "creating", + states: { + creating: { + on: { + POINTED_CANVAS: { + do: "createDot", + to: "creatingDot.positioning", + }, + }, + }, + positioning: { + onEnter: "startTranslateSession", + on: { + MOVED_POINTER: "updateTranslateSession", + PANNED_CAMERA: "updateTranslateSession", + STOPPED_POINTING: { do: "completeSession", to: "selecting" }, + CANCELLED: { + do: ["cancelSession", "deleteSelection"], + to: "selecting", + }, + }, + }, + }, + }, + creatingCircle: {}, + creatingEllipse: {}, + creatingRay: {}, + creatingLine: {}, + creatingPolyline: {}, + creatingRectangle: {}, }, conditions: { isPointingBounds(data, payload: PointerInfo) { @@ -167,6 +215,17 @@ const state = createState({ }, }, actions: { + // Shapes + createDot(data, payload: PointerInfo) { + const shape = shapeUtilityMap[ShapeType.Dot].create({ + point: screenToWorld(payload.point, data), + }) + + data.selectedIds.clear() + data.selectedIds.add(shape.id) + data.document.pages[data.currentPageId].shapes[shape.id] = shape + }, + // History enableHistory() { history.enable() @@ -240,6 +299,20 @@ const state = createState({ }, // Selection + deleteSelection(data) { + const { document, currentPageId } = data + const shapes = document.pages[currentPageId].shapes + + data.selectedIds.forEach((id) => { + delete shapes[id] + // TODO: recursively delete children + }) + + data.selectedIds.clear() + data.hoveredId = undefined + data.pointedId = undefined + }, + setHoveredId(data, payload: PointerInfo) { data.hoveredId = payload.target }, diff --git a/utils/utils.ts b/utils/utils.ts index f4ba4da5c..5caeb3dcb 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -1,3 +1,4 @@ +import React from "react" import { Data, Bounds, TransformEdge, TransformCorner } from "types" import * as svg from "./svg" import * as vec from "./vec" @@ -892,6 +893,10 @@ export function isDarwin() { return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform) } +export function metaKey(e: KeyboardEvent | React.KeyboardEvent) { + return isDarwin() ? e.metaKey : e.ctrlKey +} + export function getTransformAnchor( type: TransformEdge | TransformCorner, isFlippedX: boolean,