Improves typing on shapes utils

This commit is contained in:
Steve Ruiz 2021-05-13 19:22:16 +01:00
parent 0ec723e0d6
commit d99507de5b
9 changed files with 181 additions and 158 deletions

19
lib/shapes/base-shape.tsx Normal file
View 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
}

View file

@ -1,10 +1,13 @@
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { BaseLibShape, CircleShape, ShapeType } from "types" import { CircleShape, ShapeType } from "types"
import { boundsCache } from "./index" import { boundsCache } from "./index"
import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections"
import { createShape } from "./base-shape"
const Circle: BaseLibShape<ShapeType.Circle> = { const circle = createShape<CircleShape>({
create(props): CircleShape { create(props) {
return { return {
id: uuid(), id: uuid(),
type: ShapeType.Circle, 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) { rotate(shape) {
return shape return shape
}, },
@ -61,13 +77,13 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
return shape return shape
}, },
scale(shape, scale: number) { scale(shape, scale) {
return shape return shape
}, },
stretch(shape, scaleX: number, scaleY: number) { stretch(shape, scaleX, scaleY) {
return shape return shape
}, },
} })
export default Circle export default circle

View file

@ -1,10 +1,13 @@
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { BaseLibShape, DotShape, ShapeType } from "types" import { DotShape, ShapeType } from "types"
import { boundsCache } from "./index" import { boundsCache } from "./index"
import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections"
import { createShape } from "./base-shape"
const Dot: BaseLibShape<ShapeType.Dot> = { const dot = createShape<DotShape>({
create(props): DotShape { create(props) {
return { return {
id: uuid(), id: uuid(),
type: ShapeType.Dot, type: ShapeType.Dot,
@ -48,6 +51,14 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
return vec.dist(shape.point, test) < 4 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) { rotate(shape) {
return shape return shape
}, },
@ -64,6 +75,6 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
stretch(shape, scaleX: number, scaleY: number) { stretch(shape, scaleX: number, scaleY: number) {
return shape return shape
}, },
} })
export default Dot export default dot

View file

@ -12,6 +12,9 @@ const shapes = {
[ShapeType.Dot]: Dot, [ShapeType.Dot]: Dot,
[ShapeType.Polyline]: Polyline, [ShapeType.Polyline]: Polyline,
[ShapeType.Rectangle]: Rectangle, [ShapeType.Rectangle]: Rectangle,
[ShapeType.Ellipse]: Rectangle,
[ShapeType.Line]: Rectangle,
[ShapeType.Ray]: Rectangle,
} }
export default shapes export default shapes

View file

@ -1,10 +1,13 @@
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { BaseLibShape, PolylineShape, ShapeType } from "types" import { PolylineShape, ShapeType } from "types"
import { boundsCache } from "./index" 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> = { const polyline = createShape<PolylineShape>({
create(props): PolylineShape { create(props) {
return { return {
id: uuid(), id: uuid(),
type: ShapeType.Polyline, type: ShapeType.Polyline,
@ -57,6 +60,18 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
return true 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) { rotate(shape) {
return shape return shape
}, },
@ -73,6 +88,6 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
stretch(shape, scaleX: number, scaleY: number) { stretch(shape, scaleX: number, scaleY: number) {
return shape return shape
}, },
} })
export default Polyline export default polyline

View file

@ -1,10 +1,12 @@
import { v4 as uuid } from "uuid" import { v4 as uuid } from "uuid"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { BaseLibShape, RectangleShape, ShapeType } from "types" import { RectangleShape, ShapeType } from "types"
import { boundsCache } from "./index" import { boundsCache } from "./index"
import { boundsContained, boundsCollide } from "utils/bounds"
import { createShape } from "./base-shape"
const Rectangle: BaseLibShape<ShapeType.Rectangle> = { const rectangle = createShape<RectangleShape>({
create(props): RectangleShape { create(props) {
return { return {
id: uuid(), id: uuid(),
type: ShapeType.Rectangle, type: ShapeType.Rectangle,
@ -50,6 +52,14 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
return true return true
}, },
hitTestBounds(shape, brushBounds) {
const shapeBounds = this.getBounds(shape)
return (
boundsContained(shapeBounds, brushBounds) ||
boundsCollide(shapeBounds, brushBounds)
)
},
rotate(shape) { rotate(shape) {
return shape return shape
}, },
@ -59,13 +69,13 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
return shape return shape
}, },
scale(shape, scale: number) { scale(shape, scale) {
return shape return shape
}, },
stretch(shape, scaleX: number, scaleY: number) { stretch(shape, scaleX, scaleY) {
return shape return shape
}, },
} })
export default Rectangle export default rectangle

View file

@ -1,13 +1,9 @@
import { current } from "immer" import { current } from "immer"
import { Bounds, Data, ShapeType } from "types" import { BaseLibShape, Bounds, Data, Shapes } from "types"
import BaseSession from "./base-session" import BaseSession from "./base-session"
import Shapes from "lib/shapes" 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 {
intersectCircleBounds,
intersectPolylineBounds,
} from "utils/intersections"
interface BrushSnapshot { interface BrushSnapshot {
selectedIds: Set<string> selectedIds: Set<string>
@ -69,124 +65,13 @@ export default class BrushSession extends BaseSession {
selectedIds: new Set(data.selectedIds), selectedIds: new Set(data.selectedIds),
shapes: Object.values(pages[currentPageId].shapes) shapes: Object.values(pages[currentPageId].shapes)
.filter((shape) => !selectedIds.has(shape.id)) .filter((shape) => !selectedIds.has(shape.id))
.map((shape) => { .map((shape) => ({
switch (shape.type) { id: shape.id,
case ShapeType.Dot: { test: (brushBounds: Bounds): boolean =>
const bounds = Shapes[shape.type].getBounds(shape) (shapes[shape.type] as BaseLibShape<
Shapes[typeof shape.type]
return { >).hitTestBounds(shape, brushBounds),
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),
} }
} }
} }
/**
* 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)
}

View file

@ -44,7 +44,7 @@ export interface BaseShape {
childIndex: number childIndex: number
name: string name: string
point: number[] point: number[]
rotation: 0 rotation: number
style: Partial<React.SVGProps<SVGUseElement>> 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 ShapeIndicatorProps<T extends Shape> = ShapeSpecificProps<T>
export type BaseLibShape<K extends ShapeType> = { export type BaseLibShape<K extends Shape> = {
create(props: Partial<Shapes[K]>): Shapes[K] create(props: Partial<K>): K
getBounds(shape: Shapes[K]): Bounds getBounds(shape: K): Bounds
hitTest(shape: Shapes[K], test: number[]): boolean hitTest(shape: K, test: number[]): boolean
rotate(shape: Shapes[K]): Shapes[K] hitTestBounds(shape: K, bounds: Bounds): boolean
translate(shape: Shapes[K], delta: number[]): Shapes[K] rotate(shape: K): K
scale(shape: Shapes[K], scale: number): Shapes[K] translate(shape: K, delta: number[]): K
stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K] scale(shape: K, scale: number): K
render(shape: Shapes[K]): JSX.Element stretch(shape: K, scaleX: number, scaleY: number): K
render(shape: K): JSX.Element
} }
export interface PointerInfo { export interface PointerInfo {

63
utils/bounds.ts Normal file
View 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)
}