Improves typing on shapes utils
This commit is contained in:
parent
0ec723e0d6
commit
d99507de5b
9 changed files with 181 additions and 158 deletions
19
lib/shapes/base-shape.tsx
Normal file
19
lib/shapes/base-shape.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { Bounds, Shape } from "types"
|
||||
|
||||
export default interface BaseLibShape<K extends Shape> {
|
||||
create(props: Partial<K>): K
|
||||
getBounds(this: BaseLibShape<K>, shape: K): Bounds
|
||||
hitTest(this: BaseLibShape<K>, shape: K, test: number[]): boolean
|
||||
hitTestBounds(this: BaseLibShape<K>, shape: K, bounds: Bounds): boolean
|
||||
rotate(this: BaseLibShape<K>, shape: K): K
|
||||
translate(this: BaseLibShape<K>, shape: K, delta: number[]): K
|
||||
scale(this: BaseLibShape<K>, shape: K, scale: number): K
|
||||
stretch(this: BaseLibShape<K>, shape: K, scaleX: number, scaleY: number): K
|
||||
render(this: BaseLibShape<K>, shape: K): JSX.Element
|
||||
}
|
||||
|
||||
export function createShape<T extends Shape>(
|
||||
shape: BaseLibShape<T>
|
||||
): BaseLibShape<T> {
|
||||
return shape
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { BaseLibShape, CircleShape, ShapeType } from "types"
|
||||
import { CircleShape, ShapeType } from "types"
|
||||
import { boundsCache } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
import { createShape } from "./base-shape"
|
||||
|
||||
const Circle: BaseLibShape<ShapeType.Circle> = {
|
||||
create(props): CircleShape {
|
||||
const circle = createShape<CircleShape>({
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Circle,
|
||||
|
@ -52,6 +55,19 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
|
|||
)
|
||||
},
|
||||
|
||||
hitTestBounds(shape, bounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
|
||||
return (
|
||||
boundsContained(shapeBounds, bounds) ||
|
||||
intersectCircleBounds(
|
||||
vec.addScalar(shape.point, shape.radius),
|
||||
shape.radius,
|
||||
bounds
|
||||
).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
@ -61,13 +77,13 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
|
|||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale: number) {
|
||||
scale(shape, scale) {
|
||||
return shape
|
||||
},
|
||||
|
||||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
stretch(shape, scaleX, scaleY) {
|
||||
return shape
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default Circle
|
||||
export default circle
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { BaseLibShape, DotShape, ShapeType } from "types"
|
||||
import { DotShape, ShapeType } from "types"
|
||||
import { boundsCache } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
import { createShape } from "./base-shape"
|
||||
|
||||
const Dot: BaseLibShape<ShapeType.Dot> = {
|
||||
create(props): DotShape {
|
||||
const dot = createShape<DotShape>({
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Dot,
|
||||
|
@ -48,6 +51,14 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
|
|||
return vec.dist(shape.point, test) < 4
|
||||
},
|
||||
|
||||
hitTestBounds(this, shape, brushBounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
return (
|
||||
boundsContained(shapeBounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
@ -64,6 +75,6 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
|
|||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
return shape
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default Dot
|
||||
export default dot
|
||||
|
|
|
@ -12,6 +12,9 @@ const shapes = {
|
|||
[ShapeType.Dot]: Dot,
|
||||
[ShapeType.Polyline]: Polyline,
|
||||
[ShapeType.Rectangle]: Rectangle,
|
||||
[ShapeType.Ellipse]: Rectangle,
|
||||
[ShapeType.Line]: Rectangle,
|
||||
[ShapeType.Ray]: Rectangle,
|
||||
}
|
||||
|
||||
export default shapes
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { BaseLibShape, PolylineShape, ShapeType } from "types"
|
||||
import { PolylineShape, ShapeType } from "types"
|
||||
import { boundsCache } from "./index"
|
||||
import { intersectPolylineBounds } from "utils/intersections"
|
||||
import { boundsCollide, boundsContained } from "utils/bounds"
|
||||
import { createShape } from "./base-shape"
|
||||
|
||||
const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
||||
create(props): PolylineShape {
|
||||
const polyline = createShape<PolylineShape>({
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Polyline,
|
||||
|
@ -57,6 +60,18 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
|||
return true
|
||||
},
|
||||
|
||||
hitTestBounds(this, shape, bounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
return (
|
||||
boundsContained(shapeBounds, bounds) ||
|
||||
(boundsCollide(shapeBounds, bounds) &&
|
||||
intersectPolylineBounds(
|
||||
shape.points.map((point) => vec.add(point, shape.point)),
|
||||
bounds
|
||||
).length > 0)
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
@ -73,6 +88,6 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
|||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
return shape
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default Polyline
|
||||
export default polyline
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { BaseLibShape, RectangleShape, ShapeType } from "types"
|
||||
import { RectangleShape, ShapeType } from "types"
|
||||
import { boundsCache } from "./index"
|
||||
import { boundsContained, boundsCollide } from "utils/bounds"
|
||||
import { createShape } from "./base-shape"
|
||||
|
||||
const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
||||
create(props): RectangleShape {
|
||||
const rectangle = createShape<RectangleShape>({
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Rectangle,
|
||||
|
@ -50,6 +52,14 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
|||
return true
|
||||
},
|
||||
|
||||
hitTestBounds(shape, brushBounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
return (
|
||||
boundsContained(shapeBounds, brushBounds) ||
|
||||
boundsCollide(shapeBounds, brushBounds)
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
@ -59,13 +69,13 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
|||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale: number) {
|
||||
scale(shape, scale) {
|
||||
return shape
|
||||
},
|
||||
|
||||
stretch(shape, scaleX: number, scaleY: number) {
|
||||
stretch(shape, scaleX, scaleY) {
|
||||
return shape
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default Rectangle
|
||||
export default rectangle
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
import { current } from "immer"
|
||||
import { Bounds, Data, ShapeType } from "types"
|
||||
import { BaseLibShape, Bounds, Data, Shapes } from "types"
|
||||
import BaseSession from "./base-session"
|
||||
import Shapes from "lib/shapes"
|
||||
import shapes from "lib/shapes"
|
||||
import { getBoundsFromPoints } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import {
|
||||
intersectCircleBounds,
|
||||
intersectPolylineBounds,
|
||||
} from "utils/intersections"
|
||||
|
||||
interface BrushSnapshot {
|
||||
selectedIds: Set<string>
|
||||
|
@ -69,124 +65,13 @@ export default class BrushSession extends BaseSession {
|
|||
selectedIds: new Set(data.selectedIds),
|
||||
shapes: Object.values(pages[currentPageId].shapes)
|
||||
.filter((shape) => !selectedIds.has(shape.id))
|
||||
.map((shape) => {
|
||||
switch (shape.type) {
|
||||
case ShapeType.Dot: {
|
||||
const bounds = Shapes[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds) =>
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0,
|
||||
}
|
||||
}
|
||||
case ShapeType.Circle: {
|
||||
const bounds = Shapes[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds) =>
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
intersectCircleBounds(
|
||||
vec.addScalar(shape.point, shape.radius),
|
||||
shape.radius,
|
||||
brushBounds
|
||||
).length > 0,
|
||||
}
|
||||
}
|
||||
case ShapeType.Rectangle: {
|
||||
const bounds = Shapes[shape.type].getBounds(shape)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds) =>
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
boundsCollide(bounds, brushBounds),
|
||||
}
|
||||
}
|
||||
case ShapeType.Polyline: {
|
||||
const bounds = Shapes[shape.type].getBounds(shape)
|
||||
const points = shape.points.map((point) =>
|
||||
vec.add(point, shape.point)
|
||||
)
|
||||
|
||||
return {
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds) =>
|
||||
boundsContained(bounds, brushBounds) ||
|
||||
(boundsCollide(bounds, brushBounds) &&
|
||||
intersectPolylineBounds(points, brushBounds).length > 0),
|
||||
}
|
||||
}
|
||||
default: {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean),
|
||||
.map((shape) => ({
|
||||
id: shape.id,
|
||||
test: (brushBounds: Bounds): boolean =>
|
||||
(shapes[shape.type] as BaseLibShape<
|
||||
Shapes[typeof shape.type]
|
||||
>).hitTestBounds(shape, brushBounds),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
|
21
types.ts
21
types.ts
|
@ -44,7 +44,7 @@ export interface BaseShape {
|
|||
childIndex: number
|
||||
name: string
|
||||
point: number[]
|
||||
rotation: 0
|
||||
rotation: number
|
||||
style: Partial<React.SVGProps<SVGUseElement>>
|
||||
}
|
||||
|
||||
|
@ -120,15 +120,16 @@ export type ShapeSpecificProps<T extends Shape> = Pick<
|
|||
|
||||
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], delta: number[]): 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
|
||||
export type BaseLibShape<K extends Shape> = {
|
||||
create(props: Partial<K>): K
|
||||
getBounds(shape: K): Bounds
|
||||
hitTest(shape: K, test: number[]): boolean
|
||||
hitTestBounds(shape: K, bounds: Bounds): boolean
|
||||
rotate(shape: K): K
|
||||
translate(shape: K, delta: number[]): K
|
||||
scale(shape: K, scale: number): K
|
||||
stretch(shape: K, scaleX: number, scaleY: number): K
|
||||
render(shape: K): JSX.Element
|
||||
}
|
||||
|
||||
export interface PointerInfo {
|
||||
|
|
63
utils/bounds.ts
Normal file
63
utils/bounds.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Bounds } from "types"
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
Loading…
Reference in a new issue