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,