Greatly simplifies shapes

This commit is contained in:
Steve Ruiz 2021-05-12 22:11:17 +01:00
parent 32082492d1
commit 3d52d9e9d2
18 changed files with 438 additions and 610 deletions

View file

@ -1,35 +1,123 @@
import React, { useCallback, useRef } from "react"
import state, { useSelector } from "state"
import styled from "styles"
import { getPointerEventInfo } from "utils/utils"
import { memo } from "react" import { memo } from "react"
import { useSelector } from "state" import Shapes from "lib/shapes"
import { ShapeType } from "types"
import Circle from "./shapes/circle"
import Dot from "./shapes/dot"
import Polyline from "./shapes/polyline"
import Rectangle from "./shapes/rectangle"
/* /*
Gets the shape from the current page's shapes, using the Gets the shape from the current page's shapes, using the
provided ID. Depending on the shape's type, return the provided ID. Depending on the shape's type, return the
component for that type. component for that type.
This component takes an SVG shape as its children. It handles
events for the shape as well as provides indicators for hover
and selected status
*/ */
function Shape({ id }: { id: string }) { function Shape({ id }: { id: string }) {
const rGroup = useRef<SVGGElement>(null)
const shape = useSelector((state) => { const shape = useSelector((state) => {
const { currentPageId, document } = state.data const { currentPageId, document } = state.data
return document.pages[currentPageId].shapes[id] return document.pages[currentPageId].shapes[id]
}) })
switch (shape.type) { const isSelected = useSelector((state) => state.values.selectedIds.has(id))
case ShapeType.Dot:
return <Dot {...shape} /> const handlePointerDown = useCallback(
case ShapeType.Circle: (e: React.PointerEvent) => {
return <Circle {...shape} /> e.stopPropagation()
case ShapeType.Rectangle: rGroup.current.setPointerCapture(e.pointerId)
return <Rectangle {...shape} /> state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) })
case ShapeType.Polyline: },
return <Polyline {...shape} /> [id]
default: )
return null
} const handlePointerUp = useCallback(
(e: React.PointerEvent) => {
e.stopPropagation()
rGroup.current.releasePointerCapture(e.pointerId)
state.send("STOPPED_POINTING_SHAPE", { id, ...getPointerEventInfo(e) })
},
[id]
)
const handlePointerEnter = useCallback(
(e: React.PointerEvent) =>
state.send("HOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
[id]
)
const handlePointerLeave = useCallback(
(e: React.PointerEvent) =>
state.send("UNHOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
[id]
)
return (
<StyledGroup
ref={rGroup}
isSelected={isSelected}
transform={`translate(${shape.point})`}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
>
<defs>
{Shapes[shape.type] ? Shapes[shape.type].render(shape) : null}
</defs>
<HoverIndicator as="use" xlinkHref={"#" + id} />
<use xlinkHref={"#" + id} {...shape.style} />
<Indicator as="use" xlinkHref={"#" + id} />
</StyledGroup>
)
} }
const Indicator = styled("path", {
fill: "none",
stroke: "transparent",
strokeWidth: "max(1, calc(2 / var(--camera-zoom)))",
pointerEvents: "none",
strokeLineCap: "round",
strokeLinejoin: "round",
})
const HoverIndicator = styled("path", {
fill: "none",
stroke: "transparent",
strokeWidth: "max(1, calc(8 / var(--camera-zoom)))",
pointerEvents: "all",
strokeLinecap: "round",
strokeLinejoin: "round",
})
const StyledGroup = styled("g", {
[`& ${HoverIndicator}`]: {
opacity: "0",
},
variants: {
isSelected: {
true: {
[`& ${Indicator}`]: {
stroke: "$selected",
},
[`&:hover ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
},
},
false: {
[`&:hover ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
},
},
},
},
})
export { Indicator, HoverIndicator }
export default memo(Shape) export default memo(Shape)

View file

@ -1,33 +0,0 @@
import { CircleShape, ShapeProps } from "types"
import { Indicator, HoverIndicator } from "./indicator"
import ShapeGroup from "./shape-group"
function BaseCircle({
radius,
fill = "#999",
stroke = "none",
strokeWidth = 0,
}: ShapeProps<CircleShape>) {
return (
<>
<HoverIndicator as="circle" cx={radius} cy={radius} r={radius} />
<circle
cx={radius}
cy={radius}
r={radius}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<Indicator as="circle" cx={radius} cy={radius} r={radius} />
</>
)
}
export default function Circle({ id, point, radius }: CircleShape) {
return (
<ShapeGroup id={id} point={point}>
<BaseCircle radius={radius} />
</ShapeGroup>
)
}

View file

@ -1,34 +0,0 @@
import { Indicator, HoverIndicator } from "./indicator"
import { DotShape, ShapeProps } from "types"
import ShapeGroup from "./shape-group"
const dotRadius = 4
function BaseDot({
fill = "#999",
stroke = "none",
strokeWidth = 0,
}: ShapeProps<DotShape>) {
return (
<>
<HoverIndicator as="circle" cx={dotRadius} cy={dotRadius} r={dotRadius} />
<circle
cx={dotRadius}
cy={dotRadius}
r={dotRadius}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<Indicator as="circle" cx={dotRadius} cy={dotRadius} r={dotRadius} />
</>
)
}
export default function Dot({ id, point }: DotShape) {
return (
<ShapeGroup id={id} point={point}>
<BaseDot />
</ShapeGroup>
)
}

View file

@ -1,21 +0,0 @@
import styled from "styles"
const Indicator = styled("path", {
fill: "none",
stroke: "transparent",
strokeWidth: "max(1, calc(2 / var(--camera-zoom)))",
pointerEvents: "none",
strokeLineCap: "round",
strokeLinejoin: "round",
})
const HoverIndicator = styled("path", {
fill: "none",
stroke: "transparent",
strokeWidth: "max(1, calc(8 / var(--camera-zoom)))",
pointerEvents: "all",
strokeLinecap: "round",
strokeLinejoin: "round",
})
export { Indicator, HoverIndicator }

View file

@ -1,33 +0,0 @@
import { PolylineShape, ShapeProps } from "types"
import { Indicator, HoverIndicator } from "./indicator"
import ShapeGroup from "./shape-group"
function BasePolyline({
points,
fill = "none",
stroke = "#999",
strokeWidth = 1,
}: ShapeProps<PolylineShape>) {
return (
<>
<HoverIndicator as="polyline" points={points.toString()} />
<polyline
points={points.toString()}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeLinejoin="round"
/>
<Indicator as="polyline" points={points.toString()} />
</>
)
}
export default function Polyline({ id, point, points }: PolylineShape) {
return (
<ShapeGroup id={id} point={point}>
<BasePolyline points={points} />
</ShapeGroup>
)
}

View file

@ -1,32 +0,0 @@
import { RectangleShape, ShapeProps } from "types"
import { HoverIndicator, Indicator } from "./indicator"
import ShapeGroup from "./shape-group"
function BaseRectangle({
size,
fill = "#999",
stroke = "none",
strokeWidth = 0,
}: ShapeProps<RectangleShape>) {
return (
<>
<HoverIndicator as="rect" width={size[0]} height={size[1]} />
<rect
width={size[0]}
height={size[1]}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
<Indicator as="rect" width={size[0]} height={size[1]} />
</>
)
}
export default function Rectangle({ id, point, size }: RectangleShape) {
return (
<ShapeGroup id={id} point={point}>
<BaseRectangle size={size} />
</ShapeGroup>
)
}

View file

@ -1,87 +0,0 @@
import state, { useSelector } from "state"
import React, { useCallback, useRef } from "react"
import { getPointerEventInfo } from "utils/utils"
import { Indicator, HoverIndicator } from "./indicator"
import styled from "styles"
export default function ShapeGroup({
id,
children,
point,
}: {
id: string
children: React.ReactNode
point: number[]
}) {
const rGroup = useRef<SVGGElement>(null)
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
const handlePointerDown = useCallback(
(e: React.PointerEvent) => {
e.stopPropagation()
rGroup.current.setPointerCapture(e.pointerId)
state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) })
},
[id]
)
const handlePointerUp = useCallback(
(e: React.PointerEvent) => {
e.stopPropagation()
rGroup.current.releasePointerCapture(e.pointerId)
state.send("STOPPED_POINTING_SHAPE", { id, ...getPointerEventInfo(e) })
},
[id]
)
const handlePointerEnter = useCallback(
(e: React.PointerEvent) =>
state.send("HOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
[id]
)
const handlePointerLeave = useCallback(
(e: React.PointerEvent) =>
state.send("UNHOVERED_SHAPE", { id, ...getPointerEventInfo(e) }),
[id]
)
return (
<StyledGroup
ref={rGroup}
isSelected={isSelected}
transform={`translate(${point})`}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
>
{children}
</StyledGroup>
)
}
const StyledGroup = styled("g", {
[`& ${HoverIndicator}`]: {
opacity: "0",
},
variants: {
isSelected: {
true: {
[`& ${Indicator}`]: {
stroke: "$selected",
},
[`&:hover ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
},
},
false: {
[`&:hover ${HoverIndicator}`]: {
opacity: "1",
stroke: "$hint",
},
},
},
},
})

View file

@ -12,7 +12,7 @@ export default function StatusBar() {
return ( return (
<StatusBarContainer> <StatusBarContainer>
<States>{active.join(" | ")}</States> <Section>{active.join(" | ")}</Section>
<Section>| {log}</Section> <Section>| {log}</Section>
<Section title="Renders | Time"> <Section title="Renders | Time">
{count} | {time.toString().padStart(3, "0")} {count} | {time.toString().padStart(3, "0")}
@ -45,8 +45,6 @@ const Section = styled("div", {
overflow: "hidden", overflow: "hidden",
}) })
const States = styled("div", {})
function useRenderCount() { function useRenderCount() {
const rTime = useRef(Date.now()) const rTime = useRef(Date.now())
const rCounter = useRef(0) const rCounter = useRef(0)

64
lib/shapes/circle.tsx Normal file
View file

@ -0,0 +1,64 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { BaseLibShape, CircleShape, ShapeType } from "types"
const Circle: BaseLibShape<ShapeType.Circle> = {
create(props): CircleShape {
return {
id: uuid(),
type: ShapeType.Circle,
name: "Circle",
parentId: "page0",
childIndex: 0,
point: [0, 0],
radius: 20,
rotation: 0,
style: {},
...props,
}
},
render({ id, radius }) {
return <circle id={id} cx={radius} cy={radius} r={radius} />
},
getBounds(shape) {
const {
point: [cx, cy],
radius,
} = shape
return {
minX: cx,
maxX: cx + radius * 2,
minY: cy,
maxY: cy + radius * 2,
width: radius * 2,
height: radius * 2,
}
},
hitTest(shape, test) {
return (
vec.dist(vec.addScalar(shape.point, shape.radius), test) < shape.radius
)
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
export default Circle

60
lib/shapes/dot.tsx Normal file
View file

@ -0,0 +1,60 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { BaseLibShape, DotShape, ShapeType } from "types"
const Dot: BaseLibShape<ShapeType.Dot> = {
create(props): DotShape {
return {
id: uuid(),
type: ShapeType.Dot,
name: "Dot",
parentId: "page0",
childIndex: 0,
point: [0, 0],
rotation: 0,
style: {},
...props,
}
},
render({ id }) {
return <circle id={id} cx={4} cy={4} r={4} />
},
getBounds(shape) {
const {
point: [cx, cy],
} = shape
return {
minX: cx,
maxX: cx + 4,
minY: cy,
maxY: cy + 4,
width: 4,
height: 4,
}
},
hitTest(shape, test) {
return vec.dist(shape.point, test) < 4
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
export default Dot

13
lib/shapes/index.tsx Normal file
View file

@ -0,0 +1,13 @@
import Circle from "./circle"
import Dot from "./dot"
import Polyline from "./polyline"
import Rectangle from "./rectangle"
import { ShapeType } from "types"
export default {
[ShapeType.Circle]: Circle,
[ShapeType.Dot]: Dot,
[ShapeType.Polyline]: Polyline,
[ShapeType.Rectangle]: Rectangle,
}

69
lib/shapes/polyline.tsx Normal file
View file

@ -0,0 +1,69 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { BaseLibShape, PolylineShape, ShapeType } from "types"
const Polyline: BaseLibShape<ShapeType.Polyline> = {
create(props): PolylineShape {
return {
id: uuid(),
type: ShapeType.Polyline,
name: "Polyline",
parentId: "page0",
childIndex: 0,
point: [0, 0],
points: [[0, 0]],
rotation: 0,
style: {},
...props,
}
},
render({ id, points }) {
return <polyline id={id} points={points.toString()} />
},
getBounds(shape) {
let minX = 0
let minY = 0
let maxX = 0
let maxY = 0
for (let [x, y] of shape.points) {
minX = Math.min(x, minX)
minY = Math.min(y, minY)
maxX = Math.max(x, maxX)
maxY = Math.max(y, maxY)
}
return {
minX: minX + shape.point[0],
minY: minY + shape.point[1],
maxX: maxX + shape.point[0],
maxY: maxY + shape.point[1],
width: maxX - minX,
height: maxY - minY,
}
},
hitTest(shape) {
return true
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
export default Polyline

62
lib/shapes/rectangle.tsx Normal file
View file

@ -0,0 +1,62 @@
import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { BaseLibShape, RectangleShape, ShapeType } from "types"
const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
create(props): RectangleShape {
return {
id: uuid(),
type: ShapeType.Rectangle,
name: "Rectangle",
parentId: "page0",
childIndex: 0,
point: [0, 0],
size: [1, 1],
rotation: 0,
style: {},
...props,
}
},
render({ id, size }) {
return <rect id={id} width={size[0]} height={size[1]} />
},
getBounds(shape) {
const {
point: [x, y],
size: [width, height],
} = shape
return {
minX: x,
maxX: x + width,
minY: y,
maxY: y + height,
width,
height,
}
},
hitTest(shape) {
return true
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
export default Rectangle

View file

@ -1,4 +1,5 @@
import { Data, ShapeType } from "types" import { Data, ShapeType } from "types"
import Shapes from "lib/shapes"
export const defaultDocument: Data["document"] = { export const defaultDocument: Data["document"] = {
pages: { pages: {
@ -8,31 +9,32 @@ export const defaultDocument: Data["document"] = {
name: "Page 0", name: "Page 0",
childIndex: 0, childIndex: 0,
shapes: { shapes: {
shape0: { shape3: Shapes[ShapeType.Dot].create({
id: "shape3",
name: "Shape 3",
childIndex: 3,
point: [500, 100],
style: {
fill: "#aaa",
stroke: "#777",
strokeWidth: 1,
},
}),
shape0: Shapes[ShapeType.Circle].create({
id: "shape0", id: "shape0",
type: ShapeType.Circle,
name: "Shape 0", name: "Shape 0",
parentId: "page0",
childIndex: 1, childIndex: 1,
point: [100, 100], point: [100, 100],
radius: 50, radius: 50,
rotation: 0, style: {
}, fill: "#aaa",
shape1: { stroke: "#777",
id: "shape1", strokeWidth: 1,
type: ShapeType.Rectangle, },
name: "Shape 1", }),
parentId: "page0", shape2: Shapes[ShapeType.Polyline].create({
childIndex: 1,
point: [300, 300],
size: [200, 200],
rotation: 0,
},
shape2: {
id: "shape2", id: "shape2",
type: ShapeType.Polyline,
name: "Shape 2", name: "Shape 2",
parentId: "page0",
childIndex: 2, childIndex: 2,
point: [200, 600], point: [200, 600],
points: [ points: [
@ -40,17 +42,24 @@ export const defaultDocument: Data["document"] = {
[75, 200], [75, 200],
[100, 50], [100, 50],
], ],
rotation: 0, style: {
}, fill: "none",
shape3: { stroke: "#777",
id: "shape3", strokeWidth: 2,
type: ShapeType.Dot, },
name: "Shape 3", }),
parentId: "page0", shape1: Shapes[ShapeType.Rectangle].create({
childIndex: 3, id: "shape1",
point: [500, 100], name: "Shape 1",
rotation: 0, childIndex: 1,
}, point: [300, 300],
size: [200, 200],
style: {
fill: "#aaa",
stroke: "#777",
strokeWidth: 1,
},
}),
}, },
}, },
}, },

View file

@ -1,7 +1,7 @@
import { current } from "immer" import { current } from "immer"
import { Bounds, Data, Shape, ShapeType } from "types" import { Bounds, Data, ShapeType } from "types"
import BaseSession from "./base-session" import BaseSession from "./base-session"
import shapeUtils from "utils/shape-utils" import Shapes from "lib/shapes"
import { getBoundsFromPoints } from "utils/utils" import { getBoundsFromPoints } from "utils/utils"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { import {
@ -72,7 +72,7 @@ export default class BrushSession extends BaseSession {
.map((shape) => { .map((shape) => {
switch (shape.type) { switch (shape.type) {
case ShapeType.Dot: { case ShapeType.Dot: {
const bounds = shapeUtils[shape.type].getBounds(shape) const bounds = Shapes[shape.type].getBounds(shape)
return { return {
id: shape.id, id: shape.id,
@ -82,7 +82,7 @@ export default class BrushSession extends BaseSession {
} }
} }
case ShapeType.Circle: { case ShapeType.Circle: {
const bounds = shapeUtils[shape.type].getBounds(shape) const bounds = Shapes[shape.type].getBounds(shape)
return { return {
id: shape.id, id: shape.id,
@ -96,7 +96,7 @@ export default class BrushSession extends BaseSession {
} }
} }
case ShapeType.Rectangle: { case ShapeType.Rectangle: {
const bounds = shapeUtils[shape.type].getBounds(shape) const bounds = Shapes[shape.type].getBounds(shape)
return { return {
id: shape.id, id: shape.id,
@ -106,7 +106,7 @@ export default class BrushSession extends BaseSession {
} }
} }
case ShapeType.Polyline: { case ShapeType.Polyline: {
const bounds = shapeUtils[shape.type].getBounds(shape) const bounds = Shapes[shape.type].getBounds(shape)
const points = shape.points.map((point) => const points = shape.points.map((point) =>
vec.add(point, shape.point) vec.add(point, shape.point)
) )

View file

@ -1,3 +1,5 @@
import React from "react"
export interface Data { export interface Data {
camera: { camera: {
point: number[] point: number[]
@ -26,7 +28,7 @@ export enum ShapeType {
Ellipse = "ellipse", Ellipse = "ellipse",
Line = "line", Line = "line",
Ray = "ray", Ray = "ray",
Polyline = "Polyline", Polyline = "polyline",
Rectangle = "rectangle", Rectangle = "rectangle",
// Glob = "glob", // Glob = "glob",
// Spline = "spline", // Spline = "spline",
@ -42,6 +44,7 @@ export interface BaseShape {
name: string name: string
point: number[] point: number[]
rotation: 0 rotation: 0
style: Partial<React.SVGProps<SVGUseElement>>
} }
export interface DotShape extends BaseShape { export interface DotShape extends BaseShape {
@ -107,12 +110,6 @@ export interface Shapes extends Record<ShapeType, Shape> {
[ShapeType.Rectangle]: RectangleShape [ShapeType.Rectangle]: RectangleShape
} }
export interface BaseShapeStyles {
fill: string
stroke: string
strokeWidth: number
}
export type Difference<A, B> = A extends B ? never : A export type Difference<A, B> = A extends B ? never : A
export type ShapeSpecificProps<T extends Shape> = Pick< export type ShapeSpecificProps<T extends Shape> = Pick<
@ -120,7 +117,15 @@ export type ShapeSpecificProps<T extends Shape> = Pick<
Difference<keyof T, keyof BaseShape> Difference<keyof T, keyof BaseShape>
> >
export type ShapeProps<T extends Shape> = Partial<BaseShapeStyles> &
ShapeSpecificProps<T> & { id?: Shape["id"] }
export type ShapeIndicatorProps<T extends Shape> = ShapeSpecificProps<T> export type ShapeIndicatorProps<T extends Shape> = ShapeSpecificProps<T>
export type BaseLibShape<K extends ShapeType> = {
create(props: Partial<Shapes[K]>): Shapes[K]
getBounds(shape: Shapes[K]): Bounds
hitTest(shape: Shapes[K], test: number[]): boolean
rotate(shape: Shapes[K]): Shapes[K]
translate(shape: Shapes[K]): Shapes[K]
scale(shape: Shapes[K], scale: number): Shapes[K]
stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K]
render(shape: Shapes[K]): JSX.Element
}

View file

@ -1,302 +0,0 @@
import {
boundsCollide,
boundsContain,
pointInBounds,
} from "state/sessions/brush-session"
import { Bounds, ShapeType, Shapes } from "types"
import { intersectCircleBounds } from "./intersections"
import * as vec from "./vec"
type BaseShapeUtils<K extends ShapeType> = {
getBounds(shape: Shapes[K]): Bounds
hitTest(shape: Shapes[K], test: number[]): boolean
rotate(shape: Shapes[K]): Shapes[K]
translate(shape: Shapes[K]): Shapes[K]
scale(shape: Shapes[K], scale: number): Shapes[K]
stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K]
}
/* ----------------------- Dot ---------------------- */
const DotUtils: BaseShapeUtils<ShapeType.Dot> = {
getBounds(shape) {
const {
point: [cx, cy],
} = shape
return {
minX: cx,
maxX: cx + 4,
minY: cy,
maxY: cy + 4,
width: 4,
height: 4,
}
},
hitTest(shape, test) {
return vec.dist(shape.point, test) < 4
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
/* --------------------- Circle --------------------- */
const CircleUtils: BaseShapeUtils<ShapeType.Circle> = {
getBounds(shape) {
const {
point: [cx, cy],
radius,
} = shape
return {
minX: cx,
maxX: cx + radius * 2,
minY: cy,
maxY: cy + radius * 2,
width: radius * 2,
height: radius * 2,
}
},
hitTest(shape, test) {
return (
vec.dist(vec.addScalar(shape.point, shape.radius), test) < shape.radius
)
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
/* --------------------- Ellipse -------------------- */
const EllipseUtils: BaseShapeUtils<ShapeType.Ellipse> = {
getBounds(shape) {
return {
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
width: 0,
height: 0,
}
},
hitTest(shape) {
return true
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
/* ---------------------- Line ---------------------- */
const LineUtils: BaseShapeUtils<ShapeType.Line> = {
getBounds(shape) {
return {
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
width: 0,
height: 0,
}
},
hitTest(shape) {
return true
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
/* ----------------------- Ray ---------------------- */
const RayUtils: BaseShapeUtils<ShapeType.Ray> = {
getBounds(shape) {
return {
minX: Infinity,
minY: Infinity,
maxX: Infinity,
maxY: Infinity,
width: Infinity,
height: Infinity,
}
},
hitTest(shape) {
return true
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
/* ------------------ Line Segment ------------------ */
const PolylineUtils: BaseShapeUtils<ShapeType.Polyline> = {
getBounds(shape) {
let minX = 0
let minY = 0
let maxX = 0
let maxY = 0
for (let [x, y] of shape.points) {
minX = Math.min(x, minX)
minY = Math.min(y, minY)
maxX = Math.max(x, maxX)
maxY = Math.max(y, maxY)
}
return {
minX: minX + shape.point[0],
minY: minY + shape.point[1],
maxX: maxX + shape.point[0],
maxY: maxY + shape.point[1],
width: maxX - minX,
height: maxY - minY,
}
},
hitTest(shape) {
return true
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
/* -------------------- Rectangle ------------------- */
const RectangleUtils: BaseShapeUtils<ShapeType.Rectangle> = {
getBounds(shape) {
const {
point: [x, y],
size: [width, height],
} = shape
return {
minX: x,
maxX: x + width,
minY: y,
maxY: y + height,
width,
height,
}
},
hitTest(shape) {
return true
},
rotate(shape) {
return shape
},
translate(shape) {
return shape
},
scale(shape, scale: number) {
return shape
},
stretch(shape, scaleX: number, scaleY: number) {
return shape
},
}
const shapeUtils: { [K in ShapeType]: BaseShapeUtils<K> } = {
[ShapeType.Dot]: DotUtils,
[ShapeType.Circle]: CircleUtils,
[ShapeType.Ellipse]: EllipseUtils,
[ShapeType.Line]: LineUtils,
[ShapeType.Ray]: RayUtils,
[ShapeType.Polyline]: PolylineUtils,
[ShapeType.Rectangle]: RectangleUtils,
}
export default shapeUtils

View file

@ -844,12 +844,14 @@ export async function postJsonToEndpoint(
return await d.json() return await d.json()
} }
export function getPointerEventInfo(e: React.PointerEvent | WheelEvent) { export function getPointerEventInfo(
e: PointerEvent | React.PointerEvent | WheelEvent
) {
const { shiftKey, ctrlKey, metaKey, altKey } = e const { shiftKey, ctrlKey, metaKey, altKey } = e
return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey } return { point: [e.clientX, e.clientY], shiftKey, ctrlKey, metaKey, altKey }
} }
export function getKeyboardEventInfo(e: React.KeyboardEvent | KeyboardEvent) { export function getKeyboardEventInfo(e: KeyboardEvent | React.KeyboardEvent) {
const { shiftKey, ctrlKey, metaKey, altKey } = e const { shiftKey, ctrlKey, metaKey, altKey } = e
return { key: e.key, shiftKey, ctrlKey, metaKey, altKey } return { key: e.key, shiftKey, ctrlKey, metaKey, altKey }
} }