Adds utils, brush selection testing for shapes.
This commit is contained in:
parent
9a01f5aac6
commit
7ee3a1ef3d
11 changed files with 765 additions and 32 deletions
|
@ -2,6 +2,7 @@ import { memo } from "react"
|
|||
import { useSelector } from "state"
|
||||
import { ShapeType } from "types"
|
||||
import Circle from "./shapes/circle"
|
||||
import Dot from "./shapes/dot"
|
||||
import Rectangle from "./shapes/rectangle"
|
||||
|
||||
/*
|
||||
|
@ -17,6 +18,8 @@ function Shape({ id }: { id: string }) {
|
|||
})
|
||||
|
||||
switch (shape.type) {
|
||||
case ShapeType.Dot:
|
||||
return <Dot {...shape} />
|
||||
case ShapeType.Circle:
|
||||
return <Circle {...shape} />
|
||||
case ShapeType.Rectangle:
|
||||
|
|
|
@ -1,5 +1,49 @@
|
|||
import state, { useSelector } from "state"
|
||||
import { CircleShape } from "types"
|
||||
import ShapeGroup from "./shape-group"
|
||||
import { getPointerEventInfo } from "utils/utils"
|
||||
|
||||
export default function Circle({ point, radius }: CircleShape) {
|
||||
return <circle cx={point[0]} cy={point[1]} r={radius} fill="black" />
|
||||
interface BaseCircleProps {
|
||||
point: number[]
|
||||
radius: number
|
||||
fill?: string
|
||||
stroke?: string
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
function BaseCircle({
|
||||
point,
|
||||
radius,
|
||||
fill = "#ccc",
|
||||
stroke = "none",
|
||||
strokeWidth = 0,
|
||||
}: BaseCircleProps) {
|
||||
return (
|
||||
<circle
|
||||
cx={point[0] + strokeWidth}
|
||||
cy={point[1] + strokeWidth}
|
||||
r={radius - strokeWidth}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Circle({ id, point, radius }: CircleShape) {
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
return (
|
||||
<ShapeGroup id={id}>
|
||||
<BaseCircle point={point} radius={radius} />
|
||||
{isSelected && (
|
||||
<BaseCircle
|
||||
point={point}
|
||||
radius={radius}
|
||||
fill="none"
|
||||
stroke="blue"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
)}
|
||||
</ShapeGroup>
|
||||
)
|
||||
}
|
||||
|
|
50
components/canvas/shapes/dot.tsx
Normal file
50
components/canvas/shapes/dot.tsx
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { useSelector } from "state"
|
||||
import { DotShape } from "types"
|
||||
import ShapeGroup from "./shape-group"
|
||||
|
||||
interface BaseCircleProps {
|
||||
point: number[]
|
||||
fill?: string
|
||||
stroke?: string
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
function BaseDot({
|
||||
point,
|
||||
fill = "#ccc",
|
||||
stroke = "none",
|
||||
strokeWidth = 0,
|
||||
}: BaseCircleProps) {
|
||||
return (
|
||||
<g>
|
||||
<circle
|
||||
cx={point[0] + strokeWidth}
|
||||
cy={point[1] + strokeWidth}
|
||||
r={8}
|
||||
fill="transparent"
|
||||
stroke="none"
|
||||
strokeWidth="0"
|
||||
/>
|
||||
<circle
|
||||
cx={point[0] + strokeWidth}
|
||||
cy={point[1] + strokeWidth}
|
||||
r={Math.max(1, 4 - strokeWidth)}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Dot({ id, point }: DotShape) {
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
return (
|
||||
<ShapeGroup id={id}>
|
||||
<BaseDot point={point} />
|
||||
{isSelected && (
|
||||
<BaseDot point={point} fill="none" stroke="blue" strokeWidth={1} />
|
||||
)}
|
||||
</ShapeGroup>
|
||||
)
|
||||
}
|
|
@ -1,13 +1,49 @@
|
|||
import { useSelector } from "state"
|
||||
import { RectangleShape } from "types"
|
||||
import ShapeGroup from "./shape-group"
|
||||
|
||||
export default function Rectangle({ point, size }: RectangleShape) {
|
||||
interface BaseRectangleProps {
|
||||
point: number[]
|
||||
size: number[]
|
||||
fill?: string
|
||||
stroke?: string
|
||||
strokeWidth?: number
|
||||
}
|
||||
|
||||
function BaseRectangle({
|
||||
point,
|
||||
size,
|
||||
fill = "#ccc",
|
||||
stroke = "none",
|
||||
strokeWidth = 0,
|
||||
}: BaseRectangleProps) {
|
||||
return (
|
||||
<rect
|
||||
x={point[0]}
|
||||
y={point[1]}
|
||||
width={size[0]}
|
||||
height={size[1]}
|
||||
fill="black"
|
||||
x={point[0] + strokeWidth}
|
||||
y={point[1] + strokeWidth}
|
||||
width={size[0] - strokeWidth * 2}
|
||||
height={size[1] - strokeWidth * 2}
|
||||
fill={fill}
|
||||
stroke={stroke}
|
||||
strokeWidth={strokeWidth}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Rectangle({ id, point, size }: RectangleShape) {
|
||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||
return (
|
||||
<ShapeGroup id={id}>
|
||||
<BaseRectangle point={point} size={size} />
|
||||
{isSelected && (
|
||||
<BaseRectangle
|
||||
point={point}
|
||||
size={size}
|
||||
fill="none"
|
||||
stroke="blue"
|
||||
strokeWidth={1}
|
||||
/>
|
||||
)}
|
||||
</ShapeGroup>
|
||||
)
|
||||
}
|
||||
|
|
37
components/canvas/shapes/shape-group.tsx
Normal file
37
components/canvas/shapes/shape-group.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import React from "react"
|
||||
import state from "state"
|
||||
import { Shape } from "types"
|
||||
import { getPointerEventInfo } from "utils/utils"
|
||||
|
||||
export default function ShapeGroup({
|
||||
id,
|
||||
children,
|
||||
}: {
|
||||
id: string
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<g
|
||||
onPointerDown={(e) =>
|
||||
state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) })
|
||||
}
|
||||
onPointerUp={(e) =>
|
||||
state.send("STOPPED_POINTING_SHAPE", {
|
||||
id,
|
||||
...getPointerEventInfo(e),
|
||||
})
|
||||
}
|
||||
onPointerEnter={(e) =>
|
||||
state.send("HOVERED_SHAPE", { id, ...getPointerEventInfo(e) })
|
||||
}
|
||||
onPointerLeave={(e) =>
|
||||
state.send("UNHOVERED_SHAPE", {
|
||||
id,
|
||||
...getPointerEventInfo(e),
|
||||
})
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</g>
|
||||
)
|
||||
}
|
|
@ -38,6 +38,15 @@ export const defaultDocument: Data["document"] = {
|
|||
radius: 25,
|
||||
rotation: 0,
|
||||
},
|
||||
shape3: {
|
||||
id: "shape3",
|
||||
type: ShapeType.Dot,
|
||||
name: "Shape 3",
|
||||
parentId: "page0",
|
||||
childIndex: 3,
|
||||
point: [500, 100],
|
||||
rotation: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import { current } from "immer"
|
||||
import { Bounds, Data, Shape } from "types"
|
||||
import { Bounds, Data, Shape, ShapeType } from "types"
|
||||
import BaseSession from "./base-session"
|
||||
import { screenToWorld, getBoundsFromPoints } from "utils/utils"
|
||||
import shapeUtils from "utils/shapes"
|
||||
import { getBoundsFromPoints } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
|
||||
interface BrushSnapshot {
|
||||
selectedIds: string[]
|
||||
shapes: Shape[]
|
||||
shapes: { shape: Shape; bounds: Bounds }[]
|
||||
}
|
||||
|
||||
export default class BrushSession extends BaseSession {
|
||||
|
@ -24,21 +26,41 @@ export default class BrushSession extends BaseSession {
|
|||
update = (data: Data, point: number[]) => {
|
||||
const { origin, snapshot } = this
|
||||
|
||||
const bounds = getBoundsFromPoints(origin, point)
|
||||
|
||||
data.brush = bounds
|
||||
|
||||
const { minX: x, minY: y, width: w, height: h } = bounds
|
||||
const brushBounds = getBoundsFromPoints(origin, point)
|
||||
|
||||
data.selectedIds = [
|
||||
...snapshot.selectedIds,
|
||||
...snapshot.shapes.map((shape) => {
|
||||
return shape.id
|
||||
}),
|
||||
...snapshot.shapes
|
||||
.filter(({ shape, bounds }) => {
|
||||
switch (shape.type) {
|
||||
case ShapeType.Circle: {
|
||||
return (
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, shape.radius, brushBounds)
|
||||
.length
|
||||
)
|
||||
}
|
||||
case ShapeType.Dot: {
|
||||
return (
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length
|
||||
)
|
||||
}
|
||||
case ShapeType.Rectangle: {
|
||||
return (
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
boundsCollide(bounds, brushBounds)
|
||||
)
|
||||
}
|
||||
default: {
|
||||
return boundsContained(bounds, brushBounds)
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(({ shape }) => shape.id),
|
||||
]
|
||||
|
||||
// Narrow the the items on the screen
|
||||
data.brush = bounds
|
||||
data.brush = brushBounds
|
||||
}
|
||||
|
||||
cancel = (data: Data) => {
|
||||
|
@ -52,13 +74,105 @@ export default class BrushSession extends BaseSession {
|
|||
|
||||
static getSnapshot(data: Data) {
|
||||
const {
|
||||
selectedIds,
|
||||
document: { pages },
|
||||
currentPageId,
|
||||
} = current(data)
|
||||
|
||||
const currentlySelected = new Set(selectedIds)
|
||||
|
||||
return {
|
||||
selectedIds: [...data.selectedIds],
|
||||
shapes: Object.values(pages[currentPageId].shapes),
|
||||
shapes: Object.values(pages[currentPageId].shapes)
|
||||
.filter((shape) => !currentlySelected.has(shape.id))
|
||||
.map((shape) => {
|
||||
switch (shape.type) {
|
||||
case ShapeType.Dot: {
|
||||
return {
|
||||
shape,
|
||||
bounds: shapeUtils[shape.type].getBounds(shape),
|
||||
}
|
||||
}
|
||||
case ShapeType.Circle: {
|
||||
return {
|
||||
shape,
|
||||
bounds: shapeUtils[shape.type].getBounds(shape),
|
||||
}
|
||||
}
|
||||
case ShapeType.Rectangle: {
|
||||
return {
|
||||
shape,
|
||||
bounds: shapeUtils[shape.type].getBounds(shape),
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether two bounds collide.
|
||||
* @param a Bounds
|
||||
* @param b Bounds
|
||||
* @returns
|
||||
*/
|
||||
export function boundsCollide(a: Bounds, b: Bounds) {
|
||||
return !(
|
||||
a.maxX < b.minX ||
|
||||
a.minX > b.maxX ||
|
||||
a.maxY < b.minY ||
|
||||
a.minY > b.maxY
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the bounds of A contain the bounds of B. A perfect match will return true.
|
||||
* @param a Bounds
|
||||
* @param b Bounds
|
||||
* @returns
|
||||
*/
|
||||
export function boundsContain(a: Bounds, b: Bounds) {
|
||||
return (
|
||||
a.minX < b.minX && a.minY < b.minY && a.maxY > b.maxY && a.maxX > b.maxX
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the bounds of A are contained by the bounds of B.
|
||||
* @param a Bounds
|
||||
* @param b Bounds
|
||||
* @returns
|
||||
*/
|
||||
export function boundsContained(a: Bounds, b: Bounds) {
|
||||
return boundsContain(b, a)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether two bounds are identical.
|
||||
* @param a Bounds
|
||||
* @param b Bounds
|
||||
* @returns
|
||||
*/
|
||||
export function boundsAreEqual(a: Bounds, b: Bounds) {
|
||||
return !(
|
||||
b.maxX !== a.maxX ||
|
||||
b.minX !== a.minX ||
|
||||
b.maxY !== a.maxY ||
|
||||
b.minY !== a.minY
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether a point is inside of a bounds.
|
||||
* @param A
|
||||
* @param b
|
||||
* @returns
|
||||
*/
|
||||
export function pointInBounds(A: number[], b: Bounds) {
|
||||
return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
|
||||
}
|
||||
|
|
|
@ -35,7 +35,10 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
brushSelecting: {
|
||||
onEnter: "startBrushSession",
|
||||
onEnter: [
|
||||
{ unless: "isPressingShiftKey", do: "clearSelection" },
|
||||
"startBrushSession",
|
||||
],
|
||||
on: {
|
||||
MOVED_POINTER: "updateBrushSession",
|
||||
PANNED_CAMERA: "updateBrushSession",
|
||||
|
@ -44,6 +47,11 @@ const state = createState({
|
|||
},
|
||||
},
|
||||
},
|
||||
conditions: {
|
||||
isPressingShiftKey(data, payload: { shiftKey: boolean }) {
|
||||
return payload.shiftKey
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
cancelSession(data) {
|
||||
session.cancel(data)
|
||||
|
@ -62,6 +70,11 @@ const state = createState({
|
|||
updateBrushSession(data, payload: { point: number[] }) {
|
||||
session.update(data, screenToWorld(payload.point, data))
|
||||
},
|
||||
// Selection
|
||||
clearSelection(data) {
|
||||
data.selectedIds = []
|
||||
},
|
||||
// Camera
|
||||
zoomCamera(data, payload: { delta: number; point: number[] }) {
|
||||
const { camera } = data
|
||||
const p0 = screenToWorld(payload.point, data)
|
||||
|
@ -81,6 +94,11 @@ const state = createState({
|
|||
)
|
||||
},
|
||||
},
|
||||
values: {
|
||||
selectedIds(data) {
|
||||
return new Set(data.selectedIds)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let session: Sessions.BaseSession
|
||||
|
|
27
types.ts
27
types.ts
|
@ -21,18 +21,17 @@ export interface Page {
|
|||
}
|
||||
|
||||
export enum ShapeType {
|
||||
Dot = "dot",
|
||||
Circle = "circle",
|
||||
Ellipse = "ellipse",
|
||||
Square = "square",
|
||||
Rectangle = "rectangle",
|
||||
Line = "line",
|
||||
LineSegment = "lineSegment",
|
||||
Dot = "dot",
|
||||
Ray = "ray",
|
||||
Glob = "glob",
|
||||
Spline = "spline",
|
||||
Cubic = "cubic",
|
||||
Conic = "conic",
|
||||
LineSegment = "lineSegment",
|
||||
Rectangle = "rectangle",
|
||||
// Glob = "glob",
|
||||
// Spline = "spline",
|
||||
// Cubic = "cubic",
|
||||
// Conic = "conic",
|
||||
}
|
||||
|
||||
export interface BaseShape {
|
||||
|
@ -87,9 +86,9 @@ export interface RectangleShape extends BaseShape {
|
|||
}
|
||||
|
||||
export type Shape =
|
||||
| DotShape
|
||||
| CircleShape
|
||||
| EllipseShape
|
||||
| DotShape
|
||||
| LineShape
|
||||
| RayShape
|
||||
| LineSegmentShape
|
||||
|
@ -103,3 +102,13 @@ export interface Bounds {
|
|||
width: number
|
||||
height: number
|
||||
}
|
||||
|
||||
export interface Shapes extends Record<ShapeType, Shape> {
|
||||
[ShapeType.Dot]: DotShape
|
||||
[ShapeType.Circle]: CircleShape
|
||||
[ShapeType.Ellipse]: EllipseShape
|
||||
[ShapeType.Line]: LineShape
|
||||
[ShapeType.Ray]: RayShape
|
||||
[ShapeType.LineSegment]: LineSegmentShape
|
||||
[ShapeType.Rectangle]: RectangleShape
|
||||
}
|
||||
|
|
103
utils/intersections.ts
Normal file
103
utils/intersections.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { Bounds } from "types"
|
||||
import * as vec from "utils/vec"
|
||||
|
||||
interface Intersection {
|
||||
didIntersect: boolean
|
||||
message: string
|
||||
points: number[][]
|
||||
}
|
||||
|
||||
export function intersectCircleLine(
|
||||
c: number[],
|
||||
r: number,
|
||||
a1: number[],
|
||||
a2: number[]
|
||||
): Intersection {
|
||||
const a =
|
||||
(a2[0] - a1[0]) * (a2[0] - a1[0]) + (a2[1] - a1[1]) * (a2[1] - a1[1])
|
||||
const b =
|
||||
2 * ((a2[0] - a1[0]) * (a1[0] - c[0]) + (a2[1] - a1[1]) * (a1[1] - c[1]))
|
||||
const cc =
|
||||
c[0] * c[0] +
|
||||
c[1] * c[1] +
|
||||
a1[0] * a1[0] +
|
||||
a1[1] * a1[1] -
|
||||
2 * (c[0] * a1[0] + c[1] * a1[1]) -
|
||||
r * r
|
||||
|
||||
const deter = b * b - 4 * a * cc
|
||||
|
||||
if (deter < 0) {
|
||||
return { didIntersect: false, message: "outside", points: [] }
|
||||
}
|
||||
|
||||
if (deter === 0) {
|
||||
return { didIntersect: false, message: "tangent", points: [] }
|
||||
}
|
||||
|
||||
var e = Math.sqrt(deter)
|
||||
var u1 = (-b + e) / (2 * a)
|
||||
var u2 = (-b - e) / (2 * a)
|
||||
if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
|
||||
if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
|
||||
return { didIntersect: false, message: "outside", points: [] }
|
||||
} else {
|
||||
return { didIntersect: false, message: "inside", points: [] }
|
||||
}
|
||||
}
|
||||
|
||||
const result = { didIntersect: true, message: "intersection", points: [] }
|
||||
if (0 <= u1 && u1 <= 1) result.points.push(vec.lrp(a1, a2, u1))
|
||||
if (0 <= u2 && u2 <= 1) result.points.push(vec.lrp(a1, a2, u2))
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function intersectCircleRectangle(
|
||||
c: number[],
|
||||
r: number,
|
||||
point: number[],
|
||||
size: number[]
|
||||
): Intersection[] {
|
||||
const tl = point
|
||||
const tr = vec.add(point, [size[0], 0])
|
||||
const br = vec.add(point, size)
|
||||
const bl = vec.add(point, [0, size[1]])
|
||||
|
||||
const intersections: Intersection[] = []
|
||||
|
||||
const topIntersection = intersectCircleLine(c, r, tl, tr)
|
||||
if (topIntersection.didIntersect) {
|
||||
intersections.push({ ...topIntersection, message: "top" })
|
||||
}
|
||||
const rightIntersection = intersectCircleLine(c, r, tr, br)
|
||||
if (rightIntersection.didIntersect) {
|
||||
intersections.push({ ...rightIntersection, message: "right" })
|
||||
}
|
||||
const bottomIntersection = intersectCircleLine(c, r, bl, br)
|
||||
if (bottomIntersection.didIntersect) {
|
||||
intersections.push({ ...bottomIntersection, message: "bottom" })
|
||||
}
|
||||
const leftIntersection = intersectCircleLine(c, r, tl, bl)
|
||||
if (leftIntersection.didIntersect) {
|
||||
intersections.push({ ...leftIntersection, message: "left" })
|
||||
}
|
||||
|
||||
return intersections
|
||||
}
|
||||
|
||||
export function intersectCircleBounds(
|
||||
c: number[],
|
||||
r: number,
|
||||
bounds: Bounds
|
||||
): Intersection[] {
|
||||
const { minX, minY, width, height } = bounds
|
||||
const intersections = intersectCircleRectangle(
|
||||
c,
|
||||
r,
|
||||
[minX, minY],
|
||||
[width, height]
|
||||
)
|
||||
|
||||
return intersections
|
||||
}
|
310
utils/shapes.ts
Normal file
310
utils/shapes.ts
Normal file
|
@ -0,0 +1,310 @@
|
|||
import {
|
||||
boundsCollide,
|
||||
boundsContain,
|
||||
pointInBounds,
|
||||
} from "state/sessions/brush-session"
|
||||
import {
|
||||
Shape,
|
||||
Bounds,
|
||||
ShapeType,
|
||||
CircleShape,
|
||||
DotShape,
|
||||
RectangleShape,
|
||||
Shapes,
|
||||
EllipseShape,
|
||||
LineShape,
|
||||
RayShape,
|
||||
LineSegmentShape,
|
||||
} 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[] | Bounds): 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 - 2,
|
||||
maxX: cx + 2,
|
||||
minY: cy - 2,
|
||||
maxY: cy + 2,
|
||||
width: 4,
|
||||
height: 4,
|
||||
}
|
||||
},
|
||||
|
||||
hitTest(shape, test) {
|
||||
if ("minX" in test) {
|
||||
return pointInBounds(shape.point, 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 - radius,
|
||||
maxX: cx + radius,
|
||||
minY: cy - radius,
|
||||
maxY: cy + radius,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
}
|
||||
},
|
||||
|
||||
hitTest(shape, test) {
|
||||
if ("minX" in test) {
|
||||
const bounds = CircleUtils.getBounds(shape)
|
||||
return (
|
||||
boundsContain(bounds, test) ||
|
||||
intersectCircleBounds(shape.point, shape.radius, bounds).length > 0
|
||||
)
|
||||
}
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
/* --------------------- 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 LineSegmentUtils: BaseShapeUtils<ShapeType.LineSegment> = {
|
||||
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
|
||||
},
|
||||
}
|
||||
|
||||
/* -------------------- 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.LineSegment]: LineSegmentUtils,
|
||||
[ShapeType.Rectangle]: RectangleUtils,
|
||||
}
|
||||
|
||||
export default shapeUtils
|
Loading…
Reference in a new issue