renames shapes -> shape utils
This commit is contained in:
parent
eea9af5f31
commit
10b0c50294
22 changed files with 29 additions and 48 deletions
173
lib/shape-utils/circle.tsx
Normal file
173
lib/shape-utils/circle.tsx
Normal file
|
@ -0,0 +1,173 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { CircleShape, ShapeType, TransformCorner, TransformEdge } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
import { pointInCircle } from "utils/hitTests"
|
||||
import { getTransformAnchor, translateBounds } from "utils/utils"
|
||||
|
||||
const circle = registerShapeUtils<CircleShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Circle,
|
||||
isGenerated: false,
|
||||
name: "Circle",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
radius: 20,
|
||||
style: {
|
||||
fill: "#c6cacb",
|
||||
stroke: "#000",
|
||||
},
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
||||
render({ id, radius }) {
|
||||
return <circle id={id} cx={radius} cy={radius} r={radius} />
|
||||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (!this.boundsCache.has(shape)) {
|
||||
const { radius } = shape
|
||||
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: radius * 2,
|
||||
minY: 0,
|
||||
maxY: radius * 2,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
}
|
||||
|
||||
this.boundsCache.set(shape, bounds)
|
||||
}
|
||||
|
||||
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||
},
|
||||
|
||||
getRotatedBounds(shape) {
|
||||
return this.getBounds(shape)
|
||||
},
|
||||
|
||||
getCenter(shape) {
|
||||
return [shape.point[0] + shape.radius, shape.point[1] + shape.radius]
|
||||
},
|
||||
|
||||
hitTest(shape, point) {
|
||||
return pointInCircle(
|
||||
point,
|
||||
vec.addScalar(shape.point, shape.radius),
|
||||
shape.radius
|
||||
)
|
||||
},
|
||||
|
||||
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
|
||||
},
|
||||
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds, { type, initialShape, scaleX, scaleY }) {
|
||||
const anchor = getTransformAnchor(type, scaleX < 0, scaleY < 0)
|
||||
|
||||
// Set the new corner or position depending on the anchor
|
||||
switch (anchor) {
|
||||
case TransformCorner.TopLeft: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [
|
||||
bounds.maxX - shape.radius * 2,
|
||||
bounds.maxY - shape.radius * 2,
|
||||
]
|
||||
break
|
||||
}
|
||||
case TransformCorner.TopRight: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [bounds.minX, bounds.maxY - shape.radius * 2]
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomRight: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [
|
||||
bounds.maxX - shape.radius * 2,
|
||||
bounds.maxY - shape.radius * 2,
|
||||
]
|
||||
break
|
||||
break
|
||||
}
|
||||
case TransformCorner.BottomLeft: {
|
||||
shape.radius = Math.min(bounds.width, bounds.height) / 2
|
||||
shape.point = [bounds.maxX - shape.radius * 2, bounds.minY]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Top: {
|
||||
shape.radius = bounds.height / 2
|
||||
shape.point = [
|
||||
bounds.minX + (bounds.width / 2 - shape.radius),
|
||||
bounds.minY,
|
||||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Right: {
|
||||
shape.radius = bounds.width / 2
|
||||
shape.point = [
|
||||
bounds.maxX - shape.radius * 2,
|
||||
bounds.minY + (bounds.height / 2 - shape.radius),
|
||||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Bottom: {
|
||||
shape.radius = bounds.height / 2
|
||||
shape.point = [
|
||||
bounds.minX + (bounds.width / 2 - shape.radius),
|
||||
bounds.maxY - shape.radius * 2,
|
||||
]
|
||||
break
|
||||
}
|
||||
case TransformEdge.Left: {
|
||||
shape.radius = bounds.width / 2
|
||||
shape.point = [
|
||||
bounds.minX,
|
||||
bounds.minY + (bounds.height / 2 - shape.radius),
|
||||
]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return shape
|
||||
},
|
||||
|
||||
transformSingle(shape, bounds, info) {
|
||||
return this.transform(shape, bounds, info)
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
export default circle
|
99
lib/shape-utils/dot.tsx
Normal file
99
lib/shape-utils/dot.tsx
Normal file
|
@ -0,0 +1,99 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { DotShape, ShapeType } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
import styled from "styles"
|
||||
import { DotCircle } from "components/canvas/misc"
|
||||
import { translateBounds } from "utils/utils"
|
||||
|
||||
const dot = registerShapeUtils<DotShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Dot,
|
||||
isGenerated: false,
|
||||
name: "Dot",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "#c6cacb",
|
||||
stroke: "#000",
|
||||
},
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
||||
render({ id }) {
|
||||
return <DotCircle id={id} cx={0} cy={0} r={4} />
|
||||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (!this.boundsCache.has(shape)) {
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: 1,
|
||||
minY: 0,
|
||||
maxY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}
|
||||
|
||||
this.boundsCache.set(shape, bounds)
|
||||
}
|
||||
|
||||
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||
},
|
||||
|
||||
getRotatedBounds(shape) {
|
||||
return this.getBounds(shape)
|
||||
},
|
||||
|
||||
getCenter(shape) {
|
||||
return shape.point
|
||||
},
|
||||
|
||||
hitTest(shape, test) {
|
||||
return true
|
||||
},
|
||||
|
||||
hitTestBounds(this, shape, brushBounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
return (
|
||||
boundsContained(shapeBounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds) {
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
|
||||
return shape
|
||||
},
|
||||
|
||||
transformSingle(shape, bounds, info) {
|
||||
return this.transform(shape, bounds, info)
|
||||
},
|
||||
|
||||
canTransform: false,
|
||||
})
|
||||
|
||||
export default dot
|
126
lib/shape-utils/ellipse.tsx
Normal file
126
lib/shape-utils/ellipse.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { EllipseShape, ShapeType } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectEllipseBounds } from "utils/intersections"
|
||||
import { pointInEllipse } from "utils/hitTests"
|
||||
import {
|
||||
getBoundsFromPoints,
|
||||
getRotatedCorners,
|
||||
rotateBounds,
|
||||
translateBounds,
|
||||
} from "utils/utils"
|
||||
|
||||
const ellipse = registerShapeUtils<EllipseShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Ellipse,
|
||||
isGenerated: false,
|
||||
name: "Ellipse",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
radiusX: 20,
|
||||
radiusY: 20,
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "#c6cacb",
|
||||
stroke: "#000",
|
||||
},
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
||||
render({ id, radiusX, radiusY }) {
|
||||
return (
|
||||
<ellipse id={id} cx={radiusX} cy={radiusY} rx={radiusX} ry={radiusY} />
|
||||
)
|
||||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (!this.boundsCache.has(shape)) {
|
||||
const { radiusX, radiusY } = shape
|
||||
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: radiusX * 2,
|
||||
minY: 0,
|
||||
maxY: radiusY * 2,
|
||||
width: radiusX * 2,
|
||||
height: radiusY * 2,
|
||||
}
|
||||
|
||||
this.boundsCache.set(shape, bounds)
|
||||
}
|
||||
|
||||
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||
},
|
||||
|
||||
getRotatedBounds(shape) {
|
||||
return getBoundsFromPoints(getRotatedCorners(shape))
|
||||
},
|
||||
|
||||
getCenter(shape) {
|
||||
return [shape.point[0] + shape.radiusX, shape.point[1] + shape.radiusY]
|
||||
},
|
||||
|
||||
hitTest(shape, point) {
|
||||
return pointInEllipse(
|
||||
point,
|
||||
vec.add(shape.point, [shape.radiusX, shape.radiusY]),
|
||||
shape.radiusX,
|
||||
shape.radiusY,
|
||||
shape.rotation
|
||||
)
|
||||
},
|
||||
|
||||
hitTestBounds(this, shape, brushBounds) {
|
||||
// TODO: Account for rotation
|
||||
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
|
||||
return (
|
||||
boundsContained(shapeBounds, brushBounds) ||
|
||||
intersectEllipseBounds(
|
||||
vec.add(shape.point, [shape.radiusX, shape.radiusY]),
|
||||
shape.radiusX,
|
||||
shape.radiusY,
|
||||
brushBounds,
|
||||
shape.rotation
|
||||
).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds) {
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
shape.radiusX = bounds.width / 2
|
||||
shape.radiusY = bounds.height / 2
|
||||
|
||||
return shape
|
||||
},
|
||||
|
||||
transformSingle(shape, bounds, info) {
|
||||
return this.transform(shape, bounds, info)
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
export default ellipse
|
123
lib/shape-utils/index.tsx
Normal file
123
lib/shape-utils/index.tsx
Normal file
|
@ -0,0 +1,123 @@
|
|||
import {
|
||||
Bounds,
|
||||
BoundsSnapshot,
|
||||
Shape,
|
||||
Shapes,
|
||||
ShapeType,
|
||||
TransformCorner,
|
||||
TransformEdge,
|
||||
} from "types"
|
||||
import circle from "./circle"
|
||||
import dot from "./dot"
|
||||
import polyline from "./polyline"
|
||||
import rectangle from "./rectangle"
|
||||
import ellipse from "./ellipse"
|
||||
import line from "./line"
|
||||
import ray from "./ray"
|
||||
|
||||
/*
|
||||
Shape Utiliies
|
||||
|
||||
A shape utility is an object containing utility methods for each type of shape
|
||||
in the application. While shapes may be very different, each shape must support
|
||||
a common set of utility methods, such as hit tests or translations, that
|
||||
|
||||
Operations throughout the app will call these utility methods
|
||||
when performing tests (such as hit tests) or mutations, such as translations.
|
||||
*/
|
||||
|
||||
export interface ShapeUtility<K extends Shape> {
|
||||
// A cache for the computed bounds of this kind of shape.
|
||||
boundsCache: WeakMap<K, Bounds>
|
||||
|
||||
// Create a new shape.
|
||||
create(props: Partial<K>): K
|
||||
|
||||
// Get the bounds of the a shape.
|
||||
getBounds(this: ShapeUtility<K>, shape: K): Bounds
|
||||
|
||||
// Get the routated bounds of the a shape.
|
||||
getRotatedBounds(this: ShapeUtility<K>, shape: K): Bounds
|
||||
|
||||
// Get the center of the shape
|
||||
getCenter(this: ShapeUtility<K>, shape: K): number[]
|
||||
|
||||
// Test whether a point lies within a shape.
|
||||
hitTest(this: ShapeUtility<K>, shape: K, test: number[]): boolean
|
||||
|
||||
// Test whether bounds collide with or contain a shape.
|
||||
hitTestBounds(this: ShapeUtility<K>, shape: K, bounds: Bounds): boolean
|
||||
|
||||
// Apply a rotation to a shape.
|
||||
rotate(this: ShapeUtility<K>, shape: K): K
|
||||
|
||||
// Apply a translation to a shape.
|
||||
translate(this: ShapeUtility<K>, shape: K, delta: number[]): K
|
||||
|
||||
// Transform to fit a new bounding box.
|
||||
transform(
|
||||
this: ShapeUtility<K>,
|
||||
shape: K,
|
||||
bounds: Bounds,
|
||||
info: {
|
||||
type: TransformEdge | TransformCorner
|
||||
initialShape: K
|
||||
scaleX: number
|
||||
scaleY: number
|
||||
}
|
||||
): K
|
||||
|
||||
transformSingle(
|
||||
this: ShapeUtility<K>,
|
||||
shape: K,
|
||||
bounds: Bounds,
|
||||
info: {
|
||||
type: TransformEdge | TransformCorner
|
||||
initialShape: K
|
||||
scaleX: number
|
||||
scaleY: number
|
||||
}
|
||||
): K
|
||||
|
||||
// Apply a scale to a shape.
|
||||
scale(this: ShapeUtility<K>, shape: K, scale: number): K
|
||||
|
||||
// Render a shape to JSX.
|
||||
render(this: ShapeUtility<K>, shape: K): JSX.Element
|
||||
|
||||
// Whether to show transform controls when this shape is selected.
|
||||
canTransform: boolean
|
||||
}
|
||||
|
||||
// A mapping of shape types to shape utilities.
|
||||
const shapeUtilityMap: { [key in ShapeType]: ShapeUtility<Shapes[key]> } = {
|
||||
[ShapeType.Circle]: circle,
|
||||
[ShapeType.Dot]: dot,
|
||||
[ShapeType.Polyline]: polyline,
|
||||
[ShapeType.Rectangle]: rectangle,
|
||||
[ShapeType.Ellipse]: ellipse,
|
||||
[ShapeType.Line]: line,
|
||||
[ShapeType.Ray]: ray,
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper to retrieve a shape utility based on a shape object.
|
||||
* @param shape
|
||||
* @returns
|
||||
*/
|
||||
export function getShapeUtils(shape: Shape): ShapeUtility<typeof shape> {
|
||||
return shapeUtilityMap[shape.type]
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 default shapeUtilityMap
|
107
lib/shape-utils/line.tsx
Normal file
107
lib/shape-utils/line.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { LineShape, ShapeType } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
import { DotCircle } from "components/canvas/misc"
|
||||
import { translateBounds } from "utils/utils"
|
||||
|
||||
const line = registerShapeUtils<LineShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Line,
|
||||
isGenerated: false,
|
||||
name: "Line",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
direction: [0, 0],
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "#c6cacb",
|
||||
stroke: "#000",
|
||||
},
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
||||
render({ id, direction }) {
|
||||
const [x1, y1] = vec.add([0, 0], vec.mul(direction, 100000))
|
||||
const [x2, y2] = vec.sub([0, 0], vec.mul(direction, 100000))
|
||||
|
||||
return (
|
||||
<g id={id}>
|
||||
<line x1={x1} y1={y1} x2={x2} y2={y2} />
|
||||
<DotCircle cx={0} cy={0} r={4} />
|
||||
</g>
|
||||
)
|
||||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (!this.boundsCache.has(shape)) {
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: 1,
|
||||
minY: 0,
|
||||
maxY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}
|
||||
|
||||
this.boundsCache.set(shape, bounds)
|
||||
}
|
||||
|
||||
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||
},
|
||||
|
||||
getRotatedBounds(shape) {
|
||||
return this.getBounds(shape)
|
||||
},
|
||||
|
||||
getCenter(shape) {
|
||||
return shape.point
|
||||
},
|
||||
|
||||
hitTest(shape, test) {
|
||||
return true
|
||||
},
|
||||
|
||||
hitTestBounds(this, shape, brushBounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
return (
|
||||
boundsContained(shapeBounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds) {
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
|
||||
return shape
|
||||
},
|
||||
|
||||
transformSingle(shape, bounds, info) {
|
||||
return this.transform(shape, bounds, info)
|
||||
},
|
||||
|
||||
canTransform: false,
|
||||
})
|
||||
|
||||
export default line
|
133
lib/shape-utils/polyline.tsx
Normal file
133
lib/shape-utils/polyline.tsx
Normal file
|
@ -0,0 +1,133 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { PolylineShape, ShapeType } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { intersectPolylineBounds } from "utils/intersections"
|
||||
import {
|
||||
boundsCollide,
|
||||
boundsContained,
|
||||
boundsContainPolygon,
|
||||
} from "utils/bounds"
|
||||
import { getBoundsFromPoints, translateBounds } from "utils/utils"
|
||||
|
||||
const polyline = registerShapeUtils<PolylineShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Polyline,
|
||||
isGenerated: false,
|
||||
name: "Polyline",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
points: [[0, 0]],
|
||||
rotation: 0,
|
||||
style: {},
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
||||
render({ id, points }) {
|
||||
return <polyline id={id} points={points.toString()} />
|
||||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (!this.boundsCache.has(shape)) {
|
||||
const bounds = getBoundsFromPoints(shape.points)
|
||||
this.boundsCache.set(shape, bounds)
|
||||
}
|
||||
|
||||
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) {
|
||||
let pt = vec.sub(point, shape.point)
|
||||
let prev = shape.points[0]
|
||||
|
||||
for (let i = 1; i < shape.points.length; i++) {
|
||||
let curr = shape.points[i]
|
||||
if (vec.distanceToLineSegment(prev, curr, pt) < 4) {
|
||||
return true
|
||||
}
|
||||
prev = curr
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
hitTestBounds(this, shape, brushBounds) {
|
||||
const b = this.getBounds(shape)
|
||||
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
|
||||
|
||||
const rotatedCorners = [
|
||||
[b.minX, b.minY],
|
||||
[b.maxX, b.minY],
|
||||
[b.maxX, b.maxY],
|
||||
[b.minX, b.maxY],
|
||||
].map((point) => vec.rotWith(point, center, shape.rotation))
|
||||
|
||||
return (
|
||||
boundsContainPolygon(brushBounds, rotatedCorners) ||
|
||||
intersectPolylineBounds(
|
||||
shape.points.map((point) => vec.add(point, shape.point)),
|
||||
brushBounds
|
||||
).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(
|
||||
shape,
|
||||
bounds,
|
||||
{ initialShape, initialShapeBounds, isFlippedX, isFlippedY }
|
||||
) {
|
||||
shape.points = shape.points.map((_, i) => {
|
||||
const [x, y] = initialShape.points[i]
|
||||
|
||||
return [
|
||||
bounds.width *
|
||||
(isFlippedX
|
||||
? 1 - x / initialShapeBounds.width
|
||||
: x / initialShapeBounds.width),
|
||||
bounds.height *
|
||||
(isFlippedY
|
||||
? 1 - y / initialShapeBounds.height
|
||||
: y / initialShapeBounds.height),
|
||||
]
|
||||
})
|
||||
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
return shape
|
||||
},
|
||||
|
||||
transformSingle(shape, bounds, info) {
|
||||
return this.transform(shape, bounds, info)
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
export default polyline
|
103
lib/shape-utils/ray.tsx
Normal file
103
lib/shape-utils/ray.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { RayShape, ShapeType } from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { boundsContained } from "utils/bounds"
|
||||
import { intersectCircleBounds } from "utils/intersections"
|
||||
import { DotCircle } from "components/canvas/misc"
|
||||
import { translateBounds } from "utils/utils"
|
||||
|
||||
const ray = registerShapeUtils<RayShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Ray,
|
||||
isGenerated: false,
|
||||
name: "Ray",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
direction: [0, 1],
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "#c6cacb",
|
||||
stroke: "#000",
|
||||
strokeWidth: 1,
|
||||
},
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
||||
render({ id, direction }) {
|
||||
const [x2, y2] = vec.add([0, 0], vec.mul(direction, 100000))
|
||||
|
||||
return (
|
||||
<g id={id}>
|
||||
<line x1={0} y1={0} x2={x2} y2={y2} />
|
||||
<DotCircle cx={0} cy={0} r={4} />
|
||||
</g>
|
||||
)
|
||||
},
|
||||
|
||||
getRotatedBounds(shape) {
|
||||
return this.getBounds(shape)
|
||||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (!this.boundsCache.has(shape)) {
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: 1,
|
||||
minY: 0,
|
||||
maxY: 1,
|
||||
width: 1,
|
||||
height: 1,
|
||||
}
|
||||
|
||||
this.boundsCache.set(shape, bounds)
|
||||
}
|
||||
|
||||
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||
},
|
||||
|
||||
getCenter(shape) {
|
||||
return shape.point
|
||||
},
|
||||
|
||||
hitTest(shape, test) {
|
||||
return true
|
||||
},
|
||||
|
||||
hitTestBounds(this, shape, brushBounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
return (
|
||||
boundsContained(shapeBounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale: number) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds) {
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: false,
|
||||
})
|
||||
|
||||
export default ray
|
136
lib/shape-utils/rectangle.tsx
Normal file
136
lib/shape-utils/rectangle.tsx
Normal file
|
@ -0,0 +1,136 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import {
|
||||
RectangleShape,
|
||||
ShapeType,
|
||||
TransformCorner,
|
||||
TransformEdge,
|
||||
} from "types"
|
||||
import { registerShapeUtils } from "./index"
|
||||
import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
|
||||
import {
|
||||
getBoundsFromPoints,
|
||||
getRotatedCorners,
|
||||
rotateBounds,
|
||||
translateBounds,
|
||||
} from "utils/utils"
|
||||
|
||||
const rectangle = registerShapeUtils<RectangleShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uuid(),
|
||||
type: ShapeType.Rectangle,
|
||||
isGenerated: false,
|
||||
name: "Rectangle",
|
||||
parentId: "page0",
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
size: [1, 1],
|
||||
rotation: 0,
|
||||
style: {
|
||||
fill: "#c6cacb",
|
||||
stroke: "#000",
|
||||
},
|
||||
...props,
|
||||
}
|
||||
},
|
||||
|
||||
render({ id, size }) {
|
||||
return <rect id={id} width={size[0]} height={size[1]} />
|
||||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (!this.boundsCache.has(shape)) {
|
||||
const [width, height] = shape.size
|
||||
const bounds = {
|
||||
minX: 0,
|
||||
maxX: width,
|
||||
minY: 0,
|
||||
maxY: height,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
|
||||
this.boundsCache.set(shape, bounds)
|
||||
}
|
||||
|
||||
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) {
|
||||
return true
|
||||
},
|
||||
|
||||
hitTestBounds(shape, brushBounds) {
|
||||
const rotatedCorners = getRotatedCorners(
|
||||
this.getBounds(shape),
|
||||
shape.rotation
|
||||
)
|
||||
|
||||
return (
|
||||
boundsContainPolygon(brushBounds, rotatedCorners) ||
|
||||
boundsCollidePolygon(brushBounds, rotatedCorners)
|
||||
)
|
||||
},
|
||||
|
||||
rotate(shape) {
|
||||
return shape
|
||||
},
|
||||
|
||||
translate(shape, delta) {
|
||||
shape.point = vec.add(shape.point, delta)
|
||||
return shape
|
||||
},
|
||||
|
||||
scale(shape, scale) {
|
||||
return shape
|
||||
},
|
||||
|
||||
transform(shape, bounds, { initialShape, scaleX, scaleY }) {
|
||||
if (shape.rotation === 0) {
|
||||
shape.size = [bounds.width, bounds.height]
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
} else {
|
||||
// Center shape in resized bounds
|
||||
shape.size = vec.mul(
|
||||
initialShape.size,
|
||||
Math.min(Math.abs(scaleX), Math.abs(scaleY))
|
||||
)
|
||||
|
||||
shape.point = vec.sub(
|
||||
vec.med([bounds.minX, bounds.minY], [bounds.maxX, bounds.maxY]),
|
||||
vec.div(shape.size, 2)
|
||||
)
|
||||
}
|
||||
|
||||
// Set rotation for flipped shapes
|
||||
shape.rotation = initialShape.rotation
|
||||
if (scaleX < 0) shape.rotation *= -1
|
||||
if (scaleY < 0) shape.rotation *= -1
|
||||
|
||||
return shape
|
||||
},
|
||||
|
||||
transformSingle(shape, bounds) {
|
||||
shape.size = [bounds.width, bounds.height]
|
||||
shape.point = [bounds.minX, bounds.minY]
|
||||
return shape
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
})
|
||||
|
||||
export default rectangle
|
Loading…
Add table
Add a link
Reference in a new issue