Adds toolbar, tools shortcuts, dot creation state
This commit is contained in:
parent
1a01c47835
commit
5420b0365f
8 changed files with 271 additions and 10 deletions
|
@ -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",
|
||||||
|
|
|
@ -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
128
components/toolbar.tsx
Normal 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: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue