Greatly simplifies shapes
This commit is contained in:
parent
32082492d1
commit
3d52d9e9d2
18 changed files with 438 additions and 610 deletions
|
@ -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 { useSelector } from "state"
|
||||
import { ShapeType } from "types"
|
||||
import Circle from "./shapes/circle"
|
||||
import Dot from "./shapes/dot"
|
||||
import Polyline from "./shapes/polyline"
|
||||
import Rectangle from "./shapes/rectangle"
|
||||
import Shapes from "lib/shapes"
|
||||
|
||||
/*
|
||||
Gets the shape from the current page's shapes, using the
|
||||
provided ID. Depending on the shape's type, return the
|
||||
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 }) {
|
||||
const rGroup = useRef<SVGGElement>(null)
|
||||
|
||||
const shape = useSelector((state) => {
|
||||
const { currentPageId, document } = state.data
|
||||
return document.pages[currentPageId].shapes[id]
|
||||
})
|
||||
|
||||
switch (shape.type) {
|
||||
case ShapeType.Dot:
|
||||
return <Dot {...shape} />
|
||||
case ShapeType.Circle:
|
||||
return <Circle {...shape} />
|
||||
case ShapeType.Rectangle:
|
||||
return <Rectangle {...shape} />
|
||||
case ShapeType.Polyline:
|
||||
return <Polyline {...shape} />
|
||||
default:
|
||||
return 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(${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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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 }
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
|
@ -12,7 +12,7 @@ export default function StatusBar() {
|
|||
|
||||
return (
|
||||
<StatusBarContainer>
|
||||
<States>{active.join(" | ")}</States>
|
||||
<Section>{active.join(" | ")}</Section>
|
||||
<Section>| {log}</Section>
|
||||
<Section title="Renders | Time">
|
||||
{count} | {time.toString().padStart(3, "0")}
|
||||
|
@ -45,8 +45,6 @@ const Section = styled("div", {
|
|||
overflow: "hidden",
|
||||
})
|
||||
|
||||
const States = styled("div", {})
|
||||
|
||||
function useRenderCount() {
|
||||
const rTime = useRef(Date.now())
|
||||
const rCounter = useRef(0)
|
||||
|
|
64
lib/shapes/circle.tsx
Normal file
64
lib/shapes/circle.tsx
Normal 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
60
lib/shapes/dot.tsx
Normal 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
13
lib/shapes/index.tsx
Normal 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
69
lib/shapes/polyline.tsx
Normal 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
62
lib/shapes/rectangle.tsx
Normal 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
|
|
@ -1,4 +1,5 @@
|
|||
import { Data, ShapeType } from "types"
|
||||
import Shapes from "lib/shapes"
|
||||
|
||||
export const defaultDocument: Data["document"] = {
|
||||
pages: {
|
||||
|
@ -8,31 +9,32 @@ export const defaultDocument: Data["document"] = {
|
|||
name: "Page 0",
|
||||
childIndex: 0,
|
||||
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",
|
||||
type: ShapeType.Circle,
|
||||
name: "Shape 0",
|
||||
parentId: "page0",
|
||||
childIndex: 1,
|
||||
point: [100, 100],
|
||||
radius: 50,
|
||||
rotation: 0,
|
||||
},
|
||||
shape1: {
|
||||
id: "shape1",
|
||||
type: ShapeType.Rectangle,
|
||||
name: "Shape 1",
|
||||
parentId: "page0",
|
||||
childIndex: 1,
|
||||
point: [300, 300],
|
||||
size: [200, 200],
|
||||
rotation: 0,
|
||||
},
|
||||
shape2: {
|
||||
style: {
|
||||
fill: "#aaa",
|
||||
stroke: "#777",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
shape2: Shapes[ShapeType.Polyline].create({
|
||||
id: "shape2",
|
||||
type: ShapeType.Polyline,
|
||||
name: "Shape 2",
|
||||
parentId: "page0",
|
||||
childIndex: 2,
|
||||
point: [200, 600],
|
||||
points: [
|
||||
|
@ -40,17 +42,24 @@ export const defaultDocument: Data["document"] = {
|
|||
[75, 200],
|
||||
[100, 50],
|
||||
],
|
||||
rotation: 0,
|
||||
},
|
||||
shape3: {
|
||||
id: "shape3",
|
||||
type: ShapeType.Dot,
|
||||
name: "Shape 3",
|
||||
parentId: "page0",
|
||||
childIndex: 3,
|
||||
point: [500, 100],
|
||||
rotation: 0,
|
||||
},
|
||||
style: {
|
||||
fill: "none",
|
||||
stroke: "#777",
|
||||
strokeWidth: 2,
|
||||
},
|
||||
}),
|
||||
shape1: Shapes[ShapeType.Rectangle].create({
|
||||
id: "shape1",
|
||||
name: "Shape 1",
|
||||
childIndex: 1,
|
||||
point: [300, 300],
|
||||
size: [200, 200],
|
||||
style: {
|
||||
fill: "#aaa",
|
||||
stroke: "#777",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { current } from "immer"
|
||||
import { Bounds, Data, Shape, ShapeType } from "types"
|
||||
import { Bounds, Data, ShapeType } from "types"
|
||||
import BaseSession from "./base-session"
|
||||
import shapeUtils from "utils/shape-utils"
|
||||
import Shapes from "lib/shapes"
|
||||
import { getBoundsFromPoints } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import {
|
||||
|
@ -72,7 +72,7 @@ export default class BrushSession extends BaseSession {
|
|||
.map((shape) => {
|
||||
switch (shape.type) {
|
||||
case ShapeType.Dot: {
|
||||
const bounds = shapeUtils[shape.type].getBounds(shape)
|
||||
const bounds = Shapes[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
|
@ -82,7 +82,7 @@ export default class BrushSession extends BaseSession {
|
|||
}
|
||||
}
|
||||
case ShapeType.Circle: {
|
||||
const bounds = shapeUtils[shape.type].getBounds(shape)
|
||||
const bounds = Shapes[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
|
@ -96,7 +96,7 @@ export default class BrushSession extends BaseSession {
|
|||
}
|
||||
}
|
||||
case ShapeType.Rectangle: {
|
||||
const bounds = shapeUtils[shape.type].getBounds(shape)
|
||||
const bounds = Shapes[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
|
@ -106,7 +106,7 @@ export default class BrushSession extends BaseSession {
|
|||
}
|
||||
}
|
||||
case ShapeType.Polyline: {
|
||||
const bounds = shapeUtils[shape.type].getBounds(shape)
|
||||
const bounds = Shapes[shape.type].getBounds(shape)
|
||||
const points = shape.points.map((point) =>
|
||||
vec.add(point, shape.point)
|
||||
)
|
||||
|
|
25
types.ts
25
types.ts
|
@ -1,3 +1,5 @@
|
|||
import React from "react"
|
||||
|
||||
export interface Data {
|
||||
camera: {
|
||||
point: number[]
|
||||
|
@ -26,7 +28,7 @@ export enum ShapeType {
|
|||
Ellipse = "ellipse",
|
||||
Line = "line",
|
||||
Ray = "ray",
|
||||
Polyline = "Polyline",
|
||||
Polyline = "polyline",
|
||||
Rectangle = "rectangle",
|
||||
// Glob = "glob",
|
||||
// Spline = "spline",
|
||||
|
@ -42,6 +44,7 @@ export interface BaseShape {
|
|||
name: string
|
||||
point: number[]
|
||||
rotation: 0
|
||||
style: Partial<React.SVGProps<SVGUseElement>>
|
||||
}
|
||||
|
||||
export interface DotShape extends BaseShape {
|
||||
|
@ -107,12 +110,6 @@ export interface Shapes extends Record<ShapeType, Shape> {
|
|||
[ShapeType.Rectangle]: RectangleShape
|
||||
}
|
||||
|
||||
export interface BaseShapeStyles {
|
||||
fill: string
|
||||
stroke: string
|
||||
strokeWidth: number
|
||||
}
|
||||
|
||||
export type Difference<A, B> = A extends B ? never : A
|
||||
|
||||
export type ShapeSpecificProps<T extends Shape> = Pick<
|
||||
|
@ -120,7 +117,15 @@ export type ShapeSpecificProps<T extends Shape> = Pick<
|
|||
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 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
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -844,12 +844,14 @@ export async function postJsonToEndpoint(
|
|||
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
|
||||
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
|
||||
return { key: e.key, shiftKey, ctrlKey, metaKey, altKey }
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue