Adds toolbar, tools shortcuts, dot creation state

This commit is contained in:
Steve Ruiz 2021-05-15 14:54:27 +01:00
parent 1a01c47835
commit 5420b0365f
8 changed files with 271 additions and 10 deletions

View file

@ -180,7 +180,7 @@ export default function CodePanel() {
const PanelContainer = styled(motion.div, { const PanelContainer = styled(motion.div, {
position: "absolute", position: "absolute",
top: "8px", top: "48px",
right: "8px", right: "8px",
bottom: "48px", bottom: "48px",
backgroundColor: "$panel", backgroundColor: "$panel",

View file

@ -1,6 +1,7 @@
import useKeyboardEvents from "hooks/useKeyboardEvents" import useKeyboardEvents from "hooks/useKeyboardEvents"
import Canvas from "./canvas/canvas" import Canvas from "./canvas/canvas"
import StatusBar from "./status-bar" import StatusBar from "./status-bar"
import Toolbar from "./toolbar"
import CodePanel from "./code-panel/code-panel" import CodePanel from "./code-panel/code-panel"
export default function Editor() { export default function Editor() {
@ -10,7 +11,8 @@ export default function Editor() {
<> <>
<Canvas /> <Canvas />
<StatusBar /> <StatusBar />
<CodePanel /> <Toolbar />
{/* <CodePanel /> */}
</> </>
) )
} }

128
components/toolbar.tsx Normal file
View file

@ -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 (
<ToolbarContainer>
<Section>
<Button>
<Menu />
</Button>
<Button
isSelected={activeTool === "select"}
onClick={() => state.send("SELECTED_SELECT_TOOL")}
>
Select
</Button>
<Button
isSelected={activeTool === "dot"}
onClick={() => state.send("SELECTED_DOT_TOOL")}
>
Dot
</Button>
<Button
isSelected={activeTool === "circle"}
onClick={() => state.send("SELECTED_CIRCLE_TOOL")}
>
Circle
</Button>
<Button
isSelected={activeTool === "ellipse"}
onClick={() => state.send("SELECTED_ELLIPSE_TOOL")}
>
Ellipse
</Button>
<Button
isSelected={activeTool === "ray"}
onClick={() => state.send("SELECTED_RAY_TOOL")}
>
Ray
</Button>
<Button
isSelected={activeTool === "line"}
onClick={() => state.send("SELECTED_LINE_TOOL")}
>
Line
</Button>
<Button
isSelected={activeTool === "polyline"}
onClick={() => state.send("SELECTED_POLYLINE_TOOL")}
>
Polyline
</Button>
<Button
isSelected={activeTool === "rectangle"}
onClick={() => state.send("SELECTED_RECTANGLE_TOOL")}
>
Rectangle
</Button>
</Section>
</ToolbarContainer>
)
}
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: {},
},
},
})

View file

@ -1,13 +1,13 @@
import { useEffect } from "react" import { useEffect } from "react"
import state from "state" import state from "state"
import { getKeyboardEventInfo, isDarwin } from "utils/utils" import { getKeyboardEventInfo, isDarwin, metaKey } from "utils/utils"
export default function useKeyboardEvents() { export default function useKeyboardEvents() {
useEffect(() => { useEffect(() => {
function handleKeyDown(e: KeyboardEvent) { function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") { if (e.key === "Escape") {
state.send("CANCELLED") state.send("CANCELLED")
} else if (e.key === "z" && (isDarwin() ? e.metaKey : e.ctrlKey)) { } else if (e.key === "z" && metaKey(e)) {
if (e.shiftKey) { if (e.shiftKey) {
state.send("REDO") state.send("REDO")
} else { } 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) { function handleKeyUp(e: KeyboardEvent) {

View file

@ -24,8 +24,15 @@ const ray = createShape<RayShape>({
} }
}, },
render({ id }) { render({ id, direction }) {
return <circle id={id} cx={4} cy={4} r={4} /> const [x2, y2] = vec.add([0, 0], vec.mul(direction, 100000))
return (
<g id={id}>
<line x1={0} y1={0} x2={x2} y2={y2} />
<circle cx={0} cy={0} r={4} />
</g>
)
}, },
getBounds(shape) { getBounds(shape) {
@ -52,7 +59,7 @@ const ray = createShape<RayShape>({
}, },
hitTest(shape, test) { hitTest(shape, test) {
return vec.dist(shape.point, test) < 4 return true
}, },
hitTestBounds(this, shape, brushBounds) { hitTestBounds(this, shape, brushBounds) {

View file

@ -9,6 +9,18 @@ export const defaultDocument: Data["document"] = {
name: "Page 0", name: "Page 0",
childIndex: 0, childIndex: 0,
shapes: { 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({ // shape3: shapeUtils[ShapeType.Dot].create({
// id: "shape3", // id: "shape3",
// name: "Shape 3", // name: "Shape 3",

View file

@ -1,9 +1,17 @@
import { createSelectorHook, createState } from "@state-designer/react" import { createSelectorHook, createState } from "@state-designer/react"
import { clamp, getCommonBounds, screenToWorld } from "utils/utils" import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
import * as vec from "utils/vec" 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 { defaultDocument } from "./data"
import { getShapeUtils } from "lib/shapes" import shapeUtilityMap, { getShapeUtils } from "lib/shapes"
import history from "state/history" import history from "state/history"
import * as Sessions from "./sessions" import * as Sessions from "./sessions"
import commands from "./commands" import commands from "./commands"
@ -35,6 +43,14 @@ const state = createState({
PANNED_CAMERA: { PANNED_CAMERA: {
do: "panCamera", 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", initial: "selecting",
states: { states: {
@ -42,6 +58,7 @@ const state = createState({
on: { on: {
UNDO: { do: "undo" }, UNDO: { do: "undo" },
REDO: { do: "redo" }, REDO: { do: "redo" },
DELETED: { do: "deleteSelection" },
GENERATED_SHAPES_FROM_CODE: "setGeneratedShapes", GENERATED_SHAPES_FROM_CODE: "setGeneratedShapes",
INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize", INCREASED_CODE_FONT_SIZE: "increaseCodeFontSize",
DECREASED_CODE_FONT_SIZE: "decreaseCodeFontSize", 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: { conditions: {
isPointingBounds(data, payload: PointerInfo) { isPointingBounds(data, payload: PointerInfo) {
@ -167,6 +215,17 @@ const state = createState({
}, },
}, },
actions: { 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 // History
enableHistory() { enableHistory() {
history.enable() history.enable()
@ -240,6 +299,20 @@ const state = createState({
}, },
// Selection // 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) { setHoveredId(data, payload: PointerInfo) {
data.hoveredId = payload.target data.hoveredId = payload.target
}, },

View file

@ -1,3 +1,4 @@
import React from "react"
import { Data, Bounds, TransformEdge, TransformCorner } from "types" import { Data, Bounds, TransformEdge, TransformCorner } from "types"
import * as svg from "./svg" import * as svg from "./svg"
import * as vec from "./vec" import * as vec from "./vec"
@ -892,6 +893,10 @@ export function isDarwin() {
return /Mac|iPod|iPhone|iPad/.test(window.navigator.platform) 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( export function getTransformAnchor(
type: TransformEdge | TransformCorner, type: TransformEdge | TransformCorner,
isFlippedX: boolean, isFlippedX: boolean,