cleans up shape utils

This commit is contained in:
Steve Ruiz 2021-06-02 16:58:51 +01:00
parent 9ab86ba9ae
commit bef35b9207
13 changed files with 134 additions and 415 deletions

View file

@ -153,12 +153,6 @@ const arrow = registerShapeUtils<ArrowShape>({
)
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
shape.style.isFilled = false
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
this.boundsCache.set(shape, getBoundsFromPoints(shape.points))
@ -167,15 +161,6 @@ const arrow = registerShapeUtils<ArrowShape>({
return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getCenter(shape) {
const bounds = this.getBounds(shape)
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
},
hitTest(shape, point) {
const { start, end, bend } = shape.handles
if (shape.bend === 0) {
@ -247,16 +232,6 @@ const arrow = registerShapeUtils<ArrowShape>({
return this
},
transformSingle(shape, bounds, info) {
this.transform(shape, bounds, info)
return this
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
onHandleMove(shape, handles) {
for (let id in handles) {
const handle = handles[id]
@ -293,8 +268,12 @@ const arrow = registerShapeUtils<ArrowShape>({
return this
},
canTransform: true,
canChangeAspectRatio: true,
applyStyles(shape, style) {
Object.assign(shape.style, style)
shape.style.isFilled = false
return this
},
canStyleFill: false,
})

View file

@ -43,11 +43,6 @@ const circle = registerShapeUtils<CircleShape>({
)
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const { radius } = shape
@ -118,14 +113,7 @@ const circle = registerShapeUtils<CircleShape>({
return this
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
canTransform: true,
canChangeAspectRatio: false,
canStyleFill: true,
})
export default circle

View file

@ -37,11 +37,6 @@ const dot = registerShapeUtils<DotShape>({
return <DotCircle id={id} cx={0} cy={0} r={3} />
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = {
@ -85,19 +80,8 @@ const dot = registerShapeUtils<DotShape>({
return this
},
transformSingle(shape, bounds, info) {
this.transform(shape, bounds, info)
return this
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
canTransform: false,
canChangeAspectRatio: false,
canStyleFill: true,
})
export default dot

View file

@ -70,13 +70,6 @@ const draw = registerShapeUtils<DrawShape>({
return <path id={id} d={pathCache.get(points)} fill={styles.stroke} />
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
shape.style.isFilled = false
shape.style.dash = DashStyle.Solid
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = getBoundsFromPoints(shape.points)
@ -161,18 +154,13 @@ const draw = registerShapeUtils<DrawShape>({
return this
},
transformSingle(shape, bounds, info) {
this.transform(shape, bounds, info)
applyStyles(shape, style) {
Object.assign(shape.style, style)
shape.style.isFilled = false
shape.style.dash = DashStyle.Solid
return this
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
canTransform: true,
canChangeAspectRatio: true,
canStyleFill: false,
})

View file

@ -49,11 +49,6 @@ const ellipse = registerShapeUtils<EllipseShape>({
)
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const { radiusX, radiusY } = shape
@ -130,15 +125,6 @@ const ellipse = registerShapeUtils<EllipseShape>({
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
canTransform: true,
canChangeAspectRatio: true,
canStyleFill: true,
})
export default ellipse

View file

@ -7,7 +7,11 @@ import {
ShapeStyles,
ShapeHandle,
ShapeBinding,
BaseShape,
ShapeSpecificProps,
Mutable,
} from 'types'
import { v4 as uuid } from 'uuid'
import circle from './circle'
import dot from './dot'
import polyline from './polyline'
@ -17,6 +21,18 @@ import line from './line'
import ray from './ray'
import draw from './draw'
import arrow from './arrow'
import rectangleUtils from '../shapes/Rectangle'
import {
getBoundsCenter,
getBoundsFromPoints,
getRotatedCorners,
} from 'utils/utils'
import shape from 'components/canvas/shape'
import {
boundsCollidePolygon,
boundsContainPolygon,
pointInBounds,
} from 'utils/bounds'
/*
Shape Utiliies
@ -47,14 +63,14 @@ export interface ShapeUtility<K extends Shape> {
applyStyles(
this: ShapeUtility<K>,
shape: K,
shape: Mutable<K>,
style: Partial<ShapeStyles>
): ShapeUtility<K>
// Transform to fit a new bounding box when more than one shape is selected.
transform(
this: ShapeUtility<K>,
shape: K,
shape: Mutable<K>,
bounds: Bounds,
info: {
type: Edge | Corner
@ -68,7 +84,7 @@ export interface ShapeUtility<K extends Shape> {
// Transform a single shape to fit a new bounding box.
transformSingle(
this: ShapeUtility<K>,
shape: K,
shape: Mutable<K>,
bounds: Bounds,
info: {
type: Edge | Corner
@ -81,7 +97,7 @@ export interface ShapeUtility<K extends Shape> {
setProperty<P extends keyof K>(
this: ShapeUtility<K>,
shape: K,
shape: Mutable<K>,
prop: P,
value: K[P]
): ShapeUtility<K>
@ -89,14 +105,14 @@ export interface ShapeUtility<K extends Shape> {
// Respond when a user moves one of the shape's bound elements.
onBindingMove?(
this: ShapeUtility<K>,
shape: K,
shape: Mutable<K>,
bindings: Record<string, ShapeBinding>
): ShapeUtility<K>
// Respond when a user moves one of the shape's handles.
onHandleMove?(
this: ShapeUtility<K>,
shape: K,
shape: Mutable<K>,
handle: Partial<K['handles']>
): ShapeUtility<K>
@ -141,15 +157,109 @@ export function getShapeUtils<T extends Shape>(shape: T): ShapeUtility<T> {
return shapeUtilityMap[shape.type] as ShapeUtility<T>
}
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
return {
boundsCache: new WeakMap(),
canTransform: true,
canChangeAspectRatio: true,
canStyleFill: true,
create(props) {
return {
id: uuid(),
isGenerated: false,
point: [0, 0],
name: 'Shape',
parentId: 'page0',
childIndex: 0,
rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
...props,
} as T
},
render(shape) {
return <circle id={shape.id} />
},
transform(shape, bounds) {
shape.point = [bounds.minX, bounds.minY]
return this
},
transformSingle(shape, bounds, info) {
return this.transform(shape, bounds, info)
},
onBindingMove() {
return this
},
onHandleMove() {
return this
},
getBounds(shape) {
const [x, y] = shape.point
return {
minX: x,
minY: y,
maxX: x + 1,
maxY: y + 1,
width: 1,
height: 1,
}
},
getRotatedBounds(shape) {
return getBoundsFromPoints(
getRotatedCorners(this.getBounds(shape), shape.rotation)
)
},
getCenter(shape) {
return getBoundsCenter(this.getBounds(shape))
},
hitTest(shape, point) {
return pointInBounds(point, this.getBounds(shape))
},
hitTestBounds(shape, brushBounds) {
const rotatedCorners = getRotatedCorners(
this.getBounds(shape),
shape.rotation
)
return (
boundsContainPolygon(brushBounds, rotatedCorners) ||
boundsCollidePolygon(brushBounds, rotatedCorners)
)
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
}
}
/**
* A factory of shape utilities, with typing enforced.
* @param shape
* @returns
*/
export function registerShapeUtils<T extends Shape>(
shape: ShapeUtility<T>
): ShapeUtility<T> {
return Object.freeze(shape)
export function registerShapeUtils<K extends Shape>(
shapeUtil: Partial<ShapeUtility<K>>
): ShapeUtility<K> {
return Object.freeze({ ...getDefaultShapeUtil<K>(), ...shapeUtil })
}
export function createShape<T extends Shape>(

View file

@ -47,11 +47,6 @@ const line = registerShapeUtils<LineShape>({
)
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const bounds = {
@ -99,11 +94,6 @@ const line = registerShapeUtils<LineShape>({
return this.transform(shape, bounds, info)
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
canTransform: false,
canChangeAspectRatio: false,
canStyleFill: false,

View file

@ -33,11 +33,6 @@ const polyline = registerShapeUtils<PolylineShape>({
return <polyline id={id} points={points.toString()} />
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
this.boundsCache.set(shape, getBoundsFromPoints(shape.points))
@ -117,11 +112,6 @@ const polyline = registerShapeUtils<PolylineShape>({
return this
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
canTransform: true,
canChangeAspectRatio: true,
canStyleFill: false,

View file

@ -45,11 +45,6 @@ const ray = registerShapeUtils<RayShape>({
)
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
getRotatedBounds(shape) {
return this.getBounds(shape)
},
@ -97,11 +92,6 @@ const ray = registerShapeUtils<RayShape>({
return this.transform(shape, bounds, info)
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
canTransform: false,
canChangeAspectRatio: false,
canStyleFill: false,

View file

@ -2,12 +2,7 @@ import { v4 as uuid } from 'uuid'
import * as vec from 'utils/vec'
import { RectangleShape, ShapeType } from 'types'
import { registerShapeUtils } from './index'
import { boundsCollidePolygon, boundsContainPolygon } from 'utils/bounds'
import {
getBoundsFromPoints,
getRotatedCorners,
translateBounds,
} from 'utils/utils'
import { translateBounds } from 'utils/utils'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const rectangle = registerShapeUtils<RectangleShape>({
@ -48,11 +43,6 @@ const rectangle = registerShapeUtils<RectangleShape>({
)
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
return this
},
getBounds(shape) {
if (!this.boundsCache.has(shape)) {
const [width, height] = shape.size
@ -71,33 +61,10 @@ const rectangle = registerShapeUtils<RectangleShape>({
return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
return getBoundsFromPoints(
getRotatedCorners(this.getBounds(shape), shape.rotation)
)
},
getCenter(shape) {
const bounds = this.getRotatedBounds(shape)
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
},
hitTest(shape) {
hitTest() {
return true
},
hitTestBounds(shape, brushBounds) {
const rotatedCorners = getRotatedCorners(
this.getBounds(shape),
shape.rotation
)
return (
boundsContainPolygon(brushBounds, rotatedCorners) ||
boundsCollidePolygon(brushBounds, rotatedCorners)
)
},
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
if (shape.rotation === 0 && !shape.isAspectRatioLocked) {
shape.size = [bounds.width, bounds.height]
@ -131,15 +98,6 @@ const rectangle = registerShapeUtils<RectangleShape>({
shape.point = [bounds.minX, bounds.minY]
return this
},
setProperty(shape, prop, value) {
shape[prop] = value
return this
},
canTransform: true,
canChangeAspectRatio: true,
canStyleFill: true,
})
export default rectangle

View file

@ -1,115 +0,0 @@
interface Props {
name: string
}
interface Core {
id: string
}
interface Instance extends Props, Core {}
const defaults: Props = {
name: 'Spot',
}
const core: Core = {
id: '0',
}
class ClassInstance<T extends object = {}> implements Instance {
id = '0'
name = 'Spot'
constructor(
props: Partial<Props> &
{ [K in keyof T]: K extends keyof Core ? never : T[K] }
) {
Object.assign(this, props)
}
}
interface InstanceConstructor {
new <T extends object = {}>(
props: Partial<Props> &
{ [K in keyof T]: K extends keyof Core ? never : T[K] }
): Instance
}
function makeInstance<T extends object = {}>(
props: Partial<Props> &
{ [K in keyof T]: K extends keyof Core ? never : T[K] } &
ThisType<ClassInstance>
) {
return new ClassInstance<T>({ ...defaults, ...props, ...core })
}
function getInstance<T extends object = {}>(
props: Partial<Props> &
{ [K in keyof T]: K extends keyof Core ? never : T[K] }
) {
return { ...defaults, ...props, ...core }
}
const instance = getInstance({
name: 'Steve',
age: 93,
wag(this: Instance) {
return this.name
},
})
interface AnimalProps {
name: string
greet(this: Animal, name: string): string
}
interface AnimalCore {
id: string
sleep(this: Animal): void
}
interface Animal extends AnimalProps, AnimalCore {}
const getAnimal = <T extends object>(
props: Partial<AnimalProps> &
{ [K in keyof T]: K extends keyof AnimalCore ? never : T[K] }
): Animal & T => {
return {
// Defaults
name: 'Animal',
greet(name) {
return 'Hey ' + name
},
// Overrides
...props,
// Core
id: 'hi',
sleep() {},
}
}
const dog = getAnimal({
name: 'doggo',
greet(name) {
return 'Woof ' + this.name
},
wag() {
return 'wagging...'
},
})
dog.greet('steve')
dog.wag()
dog.sleep()
class ShapeTest {}
const shapeTest = new ShapeTest()
export default shapeTest
type Greet = (name: string) => string
const greet: Greet = (name: string | number) => {
return 'hello ' + name
}

View file

@ -1,131 +0,0 @@
import { defaultStyle } from 'lib/shape-styles'
import {
Shape,
Bounds,
BaseShape,
ShapeSpecificProps,
ShapeType,
ShapeStyles,
MutableShape,
Edge,
Corner,
ShapeBinding,
} from 'types'
import { v4 as uuid } from 'uuid'
import * as vec from 'utils/vec'
import {
getBoundsCenter,
getRotatedCorners,
getBoundsFromPoints,
} from 'utils/utils'
class ShapeUtility<K extends MutableShape> {
boundsCache = new WeakMap<K, Bounds>([])
canTransform = true
canChangeAspectRatio = true
canStyleFill = true
// Create a new shape.
create(props: Partial<K> & ShapeSpecificProps<K>): K {
return {
id: uuid(),
isGenerated: false,
point: [0, 0],
name: 'Shape',
parentId: 'page0',
childIndex: 0,
rotation: 0,
isAspectRatioLocked: false,
isLocked: false,
isHidden: false,
...props,
} as K
}
applyStyles = (shape: K, style: Partial<ShapeStyles>) => {
Object.assign(shape.style, style)
return this
}
transform = (
shape: K,
bounds: Bounds,
info: {
type: Edge | Corner
initialShape: K
scaleX: number
scaleY: number
transformOrigin: number[]
}
) => {
shape.point = [bounds.minX, bounds.minY]
return this
}
transformSingle = (
shape: K,
bounds: Bounds,
info: {
type: Edge | Corner
initialShape: K
scaleX: number
scaleY: number
transformOrigin: number[]
}
) => {
return this.transform(shape, bounds, info)
}
setProperty = <P extends keyof K>(shape: K, prop: P, value: K[P]) => {
shape[prop] = value
return this
}
onBindingMove? = (shape: K, bindings: Record<string, ShapeBinding>) => {
return this
}
onHandleMove? = (shape: K, handle: Partial<K['handles']>) => {
return this
}
render = (shape: K): JSX.Element => {
return <circle id={shape.id} />
}
// Get the bounds of the a shape.
getBounds = (shape: K): Bounds => {
const [x, y] = shape.point
return {
minX: x,
minY: y,
maxX: x + 1,
maxY: y + 1,
width: 1,
height: 1,
}
}
// Get the routated bounds of the a shape.
getRotatedBounds = (shape: K): Bounds => {
return getBoundsFromPoints(
getRotatedCorners(this.getBounds(shape), shape.rotation)
)
}
// Get the center of the shape
getCenter = (shape: K): number[] => {
return getBoundsCenter(this.getBounds(shape))
}
// Test whether a point lies within a shape.
hitTest = (shape: K, test: number[]): boolean => {
return true
}
// Test whether bounds collide with or contain a shape.
hitTestBounds = (shape: K, bounds: Bounds): boolean => {
return true
}
}

View file

@ -395,3 +395,5 @@ export type CodeControl =
export type PropsOfType<T extends object, K> = {
[K in keyof T]: T[K] extends boolean ? K : never
}[keyof T]
export type Mutable<T extends Shape> = { -readonly [K in keyof T]: T[K] }