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 { 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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
21
types.ts
21
types.ts
|
@ -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
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