Adds drawing

This commit is contained in:
Steve Ruiz 2021-05-27 18:59:40 +01:00
parent e86cd8100d
commit 7ef83dc508
13 changed files with 353 additions and 27 deletions

View file

@ -31,7 +31,7 @@ export const Layout = styled("div", {
gridTemplateRows: "auto 1fr", gridTemplateRows: "auto 1fr",
gridAutoRows: "28px", gridAutoRows: "28px",
height: "100%", height: "100%",
width: "100%", width: "auto",
minWidth: "100%", minWidth: "100%",
maxWidth: 560, maxWidth: 560,
overflow: "hidden", overflow: "hidden",
@ -41,30 +41,32 @@ export const Layout = styled("div", {
export const Header = styled("div", { export const Header = styled("div", {
pointerEvents: "all", pointerEvents: "all",
display: "grid", display: "flex",
gridTemplateColumns: "auto 1fr auto", width: "100%",
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "space-between",
borderBottom: "1px solid $border", borderBottom: "1px solid $border",
position: "relative",
"& button": {
gridColumn: "1",
gridRow: "1",
},
"& h3": { "& h3": {
gridColumn: "1 / span 3", position: "absolute",
gridRow: "1", top: 0,
left: 0,
width: "100%",
height: "100%",
textAlign: "center", textAlign: "center",
margin: "0", padding: 0,
padding: "0", margin: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: "13px", fontSize: "13px",
pointerEvents: "none",
userSelect: "none",
}, },
}) })
export const ButtonsGroup = styled("div", { export const ButtonsGroup = styled("div", {
gridRow: "1",
gridColumn: "3",
display: "flex", display: "flex",
}) })

View file

@ -98,8 +98,8 @@ const CurrentColor = styled(DropdownMenu.Trigger, {
content: "''", content: "''",
position: "absolute", position: "absolute",
top: 0, top: 0,
left: 4, left: 0,
right: 4, right: 0,
bottom: 0, bottom: 0,
pointerEvents: "none", pointerEvents: "none",
zIndex: -1, zIndex: -1,

View file

@ -73,9 +73,6 @@ function SelectedShapeStyles({}: {}) {
return ( return (
<Panel.Layout> <Panel.Layout>
<Panel.Header> <Panel.Header>
<IconButton onClick={() => state.send("TOGGLED_STYLE_PANEL_OPEN")}>
<X />
</IconButton>
<h3>Style</h3> <h3>Style</h3>
<Panel.ButtonsGroup> <Panel.ButtonsGroup>
<IconButton <IconButton
@ -85,6 +82,9 @@ function SelectedShapeStyles({}: {}) {
<Trash /> <Trash />
</IconButton> </IconButton>
</Panel.ButtonsGroup> </Panel.ButtonsGroup>
<IconButton onClick={() => state.send("TOGGLED_STYLE_PANEL_OPEN")}>
<X />
</IconButton>
</Panel.Header> </Panel.Header>
<Content> <Content>
<ColorPicker <ColorPicker
@ -112,6 +112,7 @@ const StylePanelRoot = styled(Panel.Root, {
minWidth: 1, minWidth: 1,
width: 184, width: 184,
maxWidth: 184, maxWidth: 184,
overflow: "hidden",
position: "relative", position: "relative",
variants: { variants: {

View file

@ -13,6 +13,7 @@ export default function Toolbar() {
line: "line", line: "line",
polyline: "polyline", polyline: "polyline",
rectangle: "rectangle", rectangle: "rectangle",
draw: "draw",
}) })
) )
@ -28,6 +29,12 @@ export default function Toolbar() {
> >
Select Select
</Button> </Button>
<Button
isSelected={activeTool === "draw"}
onClick={() => state.send("SELECTED_DRAW_TOOL")}
>
Draw
</Button>
<Button <Button
isSelected={activeTool === "dot"} isSelected={activeTool === "dot"}
onClick={() => state.send("SELECTED_DOT_TOOL")} onClick={() => state.send("SELECTED_DOT_TOOL")}

View file

@ -115,6 +115,10 @@ export default function useKeyboardEvents() {
break break
} }
case "d": { case "d": {
state.send("SELECTED_DRAW_TOOL", getKeyboardEventInfo(e))
break
}
case "t": {
if (metaKey(e)) { if (metaKey(e)) {
state.send("DUPLICATED", getKeyboardEventInfo(e)) state.send("DUPLICATED", getKeyboardEventInfo(e))
} else { } else {

162
lib/shape-utils/draw.tsx Normal file
View file

@ -0,0 +1,162 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { DrawShape, ShapeType } from "types"
import { registerShapeUtils } from "./index"
import { intersectPolylineBounds } from "utils/intersections"
import { boundsContainPolygon } from "utils/bounds"
import { getBoundsFromPoints, translateBounds } from "utils/utils"
const draw = registerShapeUtils<DrawShape>({
boundsCache: new WeakMap([]),
create(props) {
return {
id: uuid(),
type: ShapeType.Draw,
isGenerated: false,
name: "Draw",
parentId: "page0",
childIndex: 0,
point: [0, 0],
points: [[0, 0]],
rotation: 0,
...props,
style: {
strokeWidth: 2,
strokeLinecap: "round",
strokeLinejoin: "round",
...props.style,
fill: "transparent",
},
}
},
render({ id, points }) {
return <polyline id={id} points={points.toString()} />
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
shape.style.fill = "transparent"
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = getBoundsFromPoints(shape.points)
this.boundsCache.set(shape, bounds)
}
return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getCenter(shape) {
const bounds = this.getBounds(shape)
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
},
hitTest(shape, point) {
let pt = vec.sub(point, shape.point)
let prev = shape.points[0]
for (let i = 1; i < shape.points.length; i++) {
let curr = shape.points[i]
if (vec.distanceToLineSegment(prev, curr, pt) < 4) {
return true
}
prev = curr
}
return false
},
hitTestBounds(this, shape, brushBounds) {
const b = this.getBounds(shape)
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
const rotatedCorners = [
[b.minX, b.minY],
[b.maxX, b.minY],
[b.maxX, b.maxY],
[b.minX, b.maxY],
].map((point) => vec.rotWith(point, center, shape.rotation))
return (
boundsContainPolygon(brushBounds, rotatedCorners) ||
intersectPolylineBounds(
shape.points.map((point) => vec.add(point, shape.point)),
brushBounds
).length > 0
)
},
rotateTo(shape, rotation) {
shape.rotation = rotation
return this
},
translateTo(shape, point) {
shape.point = point
return this
},
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
const initialShapeBounds = this.boundsCache.get(initialShape)
shape.points = initialShape.points.map(([x, y]) => {
return [
bounds.width *
(scaleX < 0
? 1 - x / initialShapeBounds.width
: x / initialShapeBounds.width),
bounds.height *
(scaleY < 0
? 1 - y / initialShapeBounds.height
: y / initialShapeBounds.height),
]
})
const newBounds = getBoundsFromPoints(shape.points)
shape.point = vec.sub(
[bounds.minX, bounds.minY],
[newBounds.minX, newBounds.minY]
)
return this
},
transformSingle(shape, bounds, info) {
this.transform(shape, bounds, info)
return this
},
setParent(shape, parentId) {
shape.parentId = parentId
return this
},
setChildIndex(shape, childIndex) {
shape.childIndex = childIndex
return this
},
setPoints(shape, points) {
// const bounds = getBoundsFromPoints(points)
// const corner = [bounds.minX, bounds.minY]
// const nudged = points.map((point) => vec.sub(point, corner))
// this.boundsCache.set(shape, translategetBoundsFromPoints(nudged))
// shape.point = vec.add(shape.point, corner)
shape.points = points
return this
},
canTransform: true,
canChangeAspectRatio: true,
})
export default draw

View file

@ -16,6 +16,7 @@ import rectangle from "./rectangle"
import ellipse from "./ellipse" import ellipse from "./ellipse"
import line from "./line" import line from "./line"
import ray from "./ray" import ray from "./ray"
import draw from "./draw"
/* /*
Shape Utiliies Shape Utiliies
@ -91,6 +92,13 @@ export interface ShapeUtility<K extends Readonly<Shape>> {
childIndex: number childIndex: number
): ShapeUtility<K> ): ShapeUtility<K>
// Add a point
setPoints?(
this: ShapeUtility<K>,
shape: K,
points: number[][]
): ShapeUtility<K>
// Render a shape to JSX. // Render a shape to JSX.
render(this: ShapeUtility<K>, shape: K): JSX.Element render(this: ShapeUtility<K>, shape: K): JSX.Element
@ -119,6 +127,7 @@ const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
[ShapeType.Ellipse]: ellipse, [ShapeType.Ellipse]: ellipse,
[ShapeType.Line]: line, [ShapeType.Line]: line,
[ShapeType.Ray]: ray, [ShapeType.Ray]: ray,
[ShapeType.Draw]: draw,
} }
/** /**

View file

@ -4,6 +4,7 @@ import direct from "./direct"
import distribute from "./distribute" import distribute from "./distribute"
import generate from "./generate" import generate from "./generate"
import move from "./move" import move from "./move"
import points from "./points"
import rotate from "./rotate" import rotate from "./rotate"
import stretch from "./stretch" import stretch from "./stretch"
import style from "./style" import style from "./style"
@ -18,6 +19,7 @@ const commands = {
distribute, distribute,
generate, generate,
move, move,
points,
rotate, rotate,
stretch, stretch,
style, style,

28
state/commands/points.ts Normal file
View file

@ -0,0 +1,28 @@
import Command from "./command"
import history from "../history"
import { Data } from "types"
import { getPage } from "utils/utils"
import { getShapeUtils } from "lib/shape-utils"
export default function pointsCommand(
data: Data,
id: string,
before: number[][],
after: number[][]
) {
history.execute(
data,
new Command({
name: "set_points",
category: "canvas",
do(data) {
const shape = getPage(data).shapes[id]
getShapeUtils(shape).setPoints!(shape, after)
},
undo(data) {
const shape = getPage(data).shapes[id]
getShapeUtils(shape).setPoints!(shape, before)
},
})
)
}

View file

@ -0,0 +1,57 @@
import { current } from "immer"
import { Data, DrawShape } from "types"
import BaseSession from "./base-session"
import { getShapeUtils } from "lib/shape-utils"
import { getPage } from "utils/utils"
import * as vec from "utils/vec"
import commands from "state/commands"
export default class BrushSession extends BaseSession {
origin: number[]
points: number[][]
snapshot: DrawSnapshot
shapeId: string
constructor(data: Data, id: string, point: number[]) {
super(data)
this.shapeId = id
this.origin = point
this.points = [[0, 0]]
this.snapshot = getDrawSnapshot(data, id)
const page = getPage(data)
const shape = page.shapes[id]
getShapeUtils(shape).translateTo(shape, point)
}
update = (data: Data, point: number[]) => {
const { shapeId } = this
this.points.push(vec.sub(point, this.origin))
const page = getPage(data)
const shape = page.shapes[shapeId]
getShapeUtils(shape).setPoints!(shape, [...this.points])
}
cancel = (data: Data) => {
const { shapeId, snapshot } = this
const page = getPage(data)
const shape = page.shapes[shapeId]
getShapeUtils(shape).setPoints!(shape, snapshot.points)
}
complete = (data: Data) => {
commands.points(data, this.shapeId, this.snapshot.points, this.points)
}
}
export function getDrawSnapshot(data: Data, shapeId: string) {
const page = getPage(current(data))
const { points } = page.shapes[shapeId] as DrawShape
return {
points,
}
}
export type DrawSnapshot = ReturnType<typeof getDrawSnapshot>

View file

@ -1,17 +1,19 @@
import BaseSession from "./base-session" import BaseSession from "./base-session"
import BrushSession from "./brush-session" import BrushSession from "./brush-session"
import TranslateSession from "./translate-session" import DirectionSession from "./direction-session"
import DrawSession from "./draw-session"
import RotateSession from "./rotate-session"
import TransformSession from "./transform-session" import TransformSession from "./transform-session"
import TransformSingleSession from "./transform-single-session" import TransformSingleSession from "./transform-single-session"
import DirectionSession from "./direction-session" import TranslateSession from "./translate-session"
import RotateSession from "./rotate-session"
export { export {
BrushSession,
BaseSession, BaseSession,
TranslateSession, BrushSession,
DirectionSession,
DrawSession,
RotateSession,
TransformSession, TransformSession,
TransformSingleSession, TransformSingleSession,
DirectionSession, TranslateSession,
RotateSession,
} }

View file

@ -31,6 +31,7 @@ import {
DistributeType, DistributeType,
AlignType, AlignType,
StretchType, StretchType,
DrawShape,
} from "types" } from "types"
const initialData: Data = { const initialData: Data = {
@ -70,6 +71,7 @@ const state = createState({
do: "panCamera", do: "panCamera",
}, },
SELECTED_SELECT_TOOL: { to: "selecting" }, SELECTED_SELECT_TOOL: { to: "selecting" },
SELECTED_DRAW_TOOL: { unless: "isReadOnly", to: "draw" },
SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "dot" }, SELECTED_DOT_TOOL: { unless: "isReadOnly", to: "dot" },
SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "circle" }, SELECTED_CIRCLE_TOOL: { unless: "isReadOnly", to: "circle" },
SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "ellipse" }, SELECTED_ELLIPSE_TOOL: { unless: "isReadOnly", to: "ellipse" },
@ -246,6 +248,32 @@ const state = createState({
}, },
}, },
}, },
draw: {
initial: "creating",
states: {
creating: {
on: {
POINTED_CANVAS: {
get: "newDraw",
do: "createShape",
to: "draw.editing",
},
},
},
editing: {
onEnter: "startDrawSession",
on: {
STOPPED_POINTING: { do: "completeSession", to: "selecting" },
CANCELLED: {
do: ["cancelSession", "deleteSelectedIds"],
to: "selecting",
},
MOVED_POINTER: "updateDrawSession",
PANNED_CAMERA: "updateDrawSession",
},
},
},
},
dot: { dot: {
initial: "creating", initial: "creating",
states: { states: {
@ -451,6 +479,9 @@ const state = createState({
}, },
}, },
results: { results: {
newDraw() {
return ShapeType.Draw
},
newDot() { newDot() {
return ShapeType.Dot return ShapeType.Dot
}, },
@ -646,6 +677,19 @@ const state = createState({
session.update(data, screenToWorld(payload.point, data)) session.update(data, screenToWorld(payload.point, data))
}, },
// Drawing
startDrawSession(data) {
const id = Array.from(data.selectedIds.values())[0]
session = new Sessions.DrawSession(
data,
id,
screenToWorld(inputs.pointer.origin, data)
)
},
updateDrawSession(data, payload: PointerInfo) {
session.update(data, screenToWorld(payload.point, data))
},
/* -------------------- Selection ------------------- */ /* -------------------- Selection ------------------- */
selectAll(data) { selectAll(data) {

View file

@ -53,6 +53,7 @@ export enum ShapeType {
Ray = "ray", Ray = "ray",
Polyline = "polyline", Polyline = "polyline",
Rectangle = "rectangle", Rectangle = "rectangle",
Draw = "draw",
} }
// Consider: // Consider:
@ -111,6 +112,11 @@ export interface RectangleShape extends BaseShape {
radius: number radius: number
} }
export interface DrawShape extends BaseShape {
type: ShapeType.Draw
points: number[][]
}
export type MutableShape = export type MutableShape =
| DotShape | DotShape
| CircleShape | CircleShape
@ -118,6 +124,7 @@ export type MutableShape =
| LineShape | LineShape
| RayShape | RayShape
| PolylineShape | PolylineShape
| DrawShape
| RectangleShape | RectangleShape
export type Shape = Readonly<MutableShape> export type Shape = Readonly<MutableShape>
@ -129,6 +136,7 @@ export interface Shapes {
[ShapeType.Line]: Readonly<LineShape> [ShapeType.Line]: Readonly<LineShape>
[ShapeType.Ray]: Readonly<RayShape> [ShapeType.Ray]: Readonly<RayShape>
[ShapeType.Polyline]: Readonly<PolylineShape> [ShapeType.Polyline]: Readonly<PolylineShape>
[ShapeType.Draw]: Readonly<DrawShape>
[ShapeType.Rectangle]: Readonly<RectangleShape> [ShapeType.Rectangle]: Readonly<RectangleShape>
} }