Adds utils, brush selection testing for shapes.
This commit is contained in:
parent
9a01f5aac6
commit
7ee3a1ef3d
11 changed files with 765 additions and 32 deletions
|
@ -2,6 +2,7 @@ import { memo } from "react"
|
||||||
import { useSelector } from "state"
|
import { useSelector } from "state"
|
||||||
import { ShapeType } from "types"
|
import { ShapeType } from "types"
|
||||||
import Circle from "./shapes/circle"
|
import Circle from "./shapes/circle"
|
||||||
|
import Dot from "./shapes/dot"
|
||||||
import Rectangle from "./shapes/rectangle"
|
import Rectangle from "./shapes/rectangle"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -17,6 +18,8 @@ function Shape({ id }: { id: string }) {
|
||||||
})
|
})
|
||||||
|
|
||||||
switch (shape.type) {
|
switch (shape.type) {
|
||||||
|
case ShapeType.Dot:
|
||||||
|
return <Dot {...shape} />
|
||||||
case ShapeType.Circle:
|
case ShapeType.Circle:
|
||||||
return <Circle {...shape} />
|
return <Circle {...shape} />
|
||||||
case ShapeType.Rectangle:
|
case ShapeType.Rectangle:
|
||||||
|
|
|
@ -1,5 +1,49 @@
|
||||||
|
import state, { useSelector } from "state"
|
||||||
import { CircleShape } from "types"
|
import { CircleShape } from "types"
|
||||||
|
import ShapeGroup from "./shape-group"
|
||||||
|
import { getPointerEventInfo } from "utils/utils"
|
||||||
|
|
||||||
export default function Circle({ point, radius }: CircleShape) {
|
interface BaseCircleProps {
|
||||||
return <circle cx={point[0]} cy={point[1]} r={radius} fill="black" />
|
point: number[]
|
||||||
|
radius: number
|
||||||
|
fill?: string
|
||||||
|
stroke?: string
|
||||||
|
strokeWidth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function BaseCircle({
|
||||||
|
point,
|
||||||
|
radius,
|
||||||
|
fill = "#ccc",
|
||||||
|
stroke = "none",
|
||||||
|
strokeWidth = 0,
|
||||||
|
}: BaseCircleProps) {
|
||||||
|
return (
|
||||||
|
<circle
|
||||||
|
cx={point[0] + strokeWidth}
|
||||||
|
cy={point[1] + strokeWidth}
|
||||||
|
r={radius - strokeWidth}
|
||||||
|
fill={fill}
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Circle({ id, point, radius }: CircleShape) {
|
||||||
|
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||||
|
return (
|
||||||
|
<ShapeGroup id={id}>
|
||||||
|
<BaseCircle point={point} radius={radius} />
|
||||||
|
{isSelected && (
|
||||||
|
<BaseCircle
|
||||||
|
point={point}
|
||||||
|
radius={radius}
|
||||||
|
fill="none"
|
||||||
|
stroke="blue"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ShapeGroup>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
50
components/canvas/shapes/dot.tsx
Normal file
50
components/canvas/shapes/dot.tsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import { useSelector } from "state"
|
||||||
|
import { DotShape } from "types"
|
||||||
|
import ShapeGroup from "./shape-group"
|
||||||
|
|
||||||
|
interface BaseCircleProps {
|
||||||
|
point: number[]
|
||||||
|
fill?: string
|
||||||
|
stroke?: string
|
||||||
|
strokeWidth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function BaseDot({
|
||||||
|
point,
|
||||||
|
fill = "#ccc",
|
||||||
|
stroke = "none",
|
||||||
|
strokeWidth = 0,
|
||||||
|
}: BaseCircleProps) {
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
<circle
|
||||||
|
cx={point[0] + strokeWidth}
|
||||||
|
cy={point[1] + strokeWidth}
|
||||||
|
r={8}
|
||||||
|
fill="transparent"
|
||||||
|
stroke="none"
|
||||||
|
strokeWidth="0"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
cx={point[0] + strokeWidth}
|
||||||
|
cy={point[1] + strokeWidth}
|
||||||
|
r={Math.max(1, 4 - strokeWidth)}
|
||||||
|
fill={fill}
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Dot({ id, point }: DotShape) {
|
||||||
|
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||||
|
return (
|
||||||
|
<ShapeGroup id={id}>
|
||||||
|
<BaseDot point={point} />
|
||||||
|
{isSelected && (
|
||||||
|
<BaseDot point={point} fill="none" stroke="blue" strokeWidth={1} />
|
||||||
|
)}
|
||||||
|
</ShapeGroup>
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,13 +1,49 @@
|
||||||
|
import { useSelector } from "state"
|
||||||
import { RectangleShape } from "types"
|
import { RectangleShape } from "types"
|
||||||
|
import ShapeGroup from "./shape-group"
|
||||||
|
|
||||||
export default function Rectangle({ point, size }: RectangleShape) {
|
interface BaseRectangleProps {
|
||||||
|
point: number[]
|
||||||
|
size: number[]
|
||||||
|
fill?: string
|
||||||
|
stroke?: string
|
||||||
|
strokeWidth?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function BaseRectangle({
|
||||||
|
point,
|
||||||
|
size,
|
||||||
|
fill = "#ccc",
|
||||||
|
stroke = "none",
|
||||||
|
strokeWidth = 0,
|
||||||
|
}: BaseRectangleProps) {
|
||||||
return (
|
return (
|
||||||
<rect
|
<rect
|
||||||
x={point[0]}
|
x={point[0] + strokeWidth}
|
||||||
y={point[1]}
|
y={point[1] + strokeWidth}
|
||||||
width={size[0]}
|
width={size[0] - strokeWidth * 2}
|
||||||
height={size[1]}
|
height={size[1] - strokeWidth * 2}
|
||||||
fill="black"
|
fill={fill}
|
||||||
|
stroke={stroke}
|
||||||
|
strokeWidth={strokeWidth}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function Rectangle({ id, point, size }: RectangleShape) {
|
||||||
|
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||||
|
return (
|
||||||
|
<ShapeGroup id={id}>
|
||||||
|
<BaseRectangle point={point} size={size} />
|
||||||
|
{isSelected && (
|
||||||
|
<BaseRectangle
|
||||||
|
point={point}
|
||||||
|
size={size}
|
||||||
|
fill="none"
|
||||||
|
stroke="blue"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ShapeGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
37
components/canvas/shapes/shape-group.tsx
Normal file
37
components/canvas/shapes/shape-group.tsx
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import React from "react"
|
||||||
|
import state from "state"
|
||||||
|
import { Shape } from "types"
|
||||||
|
import { getPointerEventInfo } from "utils/utils"
|
||||||
|
|
||||||
|
export default function ShapeGroup({
|
||||||
|
id,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
id: string
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<g
|
||||||
|
onPointerDown={(e) =>
|
||||||
|
state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) })
|
||||||
|
}
|
||||||
|
onPointerUp={(e) =>
|
||||||
|
state.send("STOPPED_POINTING_SHAPE", {
|
||||||
|
id,
|
||||||
|
...getPointerEventInfo(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
onPointerEnter={(e) =>
|
||||||
|
state.send("HOVERED_SHAPE", { id, ...getPointerEventInfo(e) })
|
||||||
|
}
|
||||||
|
onPointerLeave={(e) =>
|
||||||
|
state.send("UNHOVERED_SHAPE", {
|
||||||
|
id,
|
||||||
|
...getPointerEventInfo(e),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
|
@ -38,6 +38,15 @@ export const defaultDocument: Data["document"] = {
|
||||||
radius: 25,
|
radius: 25,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
},
|
},
|
||||||
|
shape3: {
|
||||||
|
id: "shape3",
|
||||||
|
type: ShapeType.Dot,
|
||||||
|
name: "Shape 3",
|
||||||
|
parentId: "page0",
|
||||||
|
childIndex: 3,
|
||||||
|
point: [500, 100],
|
||||||
|
rotation: 0,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { current } from "immer"
|
import { current } from "immer"
|
||||||
import { Bounds, Data, Shape } from "types"
|
import { Bounds, Data, Shape, ShapeType } from "types"
|
||||||
import BaseSession from "./base-session"
|
import BaseSession from "./base-session"
|
||||||
import { screenToWorld, getBoundsFromPoints } from "utils/utils"
|
import shapeUtils from "utils/shapes"
|
||||||
|
import { getBoundsFromPoints } from "utils/utils"
|
||||||
import * as vec from "utils/vec"
|
import * as vec from "utils/vec"
|
||||||
|
import { intersectCircleBounds } from "utils/intersections"
|
||||||
|
|
||||||
interface BrushSnapshot {
|
interface BrushSnapshot {
|
||||||
selectedIds: string[]
|
selectedIds: string[]
|
||||||
shapes: Shape[]
|
shapes: { shape: Shape; bounds: Bounds }[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class BrushSession extends BaseSession {
|
export default class BrushSession extends BaseSession {
|
||||||
|
@ -24,21 +26,41 @@ export default class BrushSession extends BaseSession {
|
||||||
update = (data: Data, point: number[]) => {
|
update = (data: Data, point: number[]) => {
|
||||||
const { origin, snapshot } = this
|
const { origin, snapshot } = this
|
||||||
|
|
||||||
const bounds = getBoundsFromPoints(origin, point)
|
const brushBounds = getBoundsFromPoints(origin, point)
|
||||||
|
|
||||||
data.brush = bounds
|
|
||||||
|
|
||||||
const { minX: x, minY: y, width: w, height: h } = bounds
|
|
||||||
|
|
||||||
data.selectedIds = [
|
data.selectedIds = [
|
||||||
...snapshot.selectedIds,
|
...snapshot.selectedIds,
|
||||||
...snapshot.shapes.map((shape) => {
|
...snapshot.shapes
|
||||||
return shape.id
|
.filter(({ shape, bounds }) => {
|
||||||
}),
|
switch (shape.type) {
|
||||||
|
case ShapeType.Circle: {
|
||||||
|
return (
|
||||||
|
boundsContained(bounds, brushBounds) ||
|
||||||
|
intersectCircleBounds(shape.point, shape.radius, brushBounds)
|
||||||
|
.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case ShapeType.Dot: {
|
||||||
|
return (
|
||||||
|
boundsContained(bounds, brushBounds) ||
|
||||||
|
intersectCircleBounds(shape.point, 4, brushBounds).length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case ShapeType.Rectangle: {
|
||||||
|
return (
|
||||||
|
boundsContained(bounds, brushBounds) ||
|
||||||
|
boundsCollide(bounds, brushBounds)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return boundsContained(bounds, brushBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(({ shape }) => shape.id),
|
||||||
]
|
]
|
||||||
|
|
||||||
// Narrow the the items on the screen
|
data.brush = brushBounds
|
||||||
data.brush = bounds
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel = (data: Data) => {
|
cancel = (data: Data) => {
|
||||||
|
@ -52,13 +74,105 @@ export default class BrushSession extends BaseSession {
|
||||||
|
|
||||||
static getSnapshot(data: Data) {
|
static getSnapshot(data: Data) {
|
||||||
const {
|
const {
|
||||||
|
selectedIds,
|
||||||
document: { pages },
|
document: { pages },
|
||||||
currentPageId,
|
currentPageId,
|
||||||
} = current(data)
|
} = current(data)
|
||||||
|
|
||||||
|
const currentlySelected = new Set(selectedIds)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
selectedIds: [...data.selectedIds],
|
selectedIds: [...data.selectedIds],
|
||||||
shapes: Object.values(pages[currentPageId].shapes),
|
shapes: Object.values(pages[currentPageId].shapes)
|
||||||
|
.filter((shape) => !currentlySelected.has(shape.id))
|
||||||
|
.map((shape) => {
|
||||||
|
switch (shape.type) {
|
||||||
|
case ShapeType.Dot: {
|
||||||
|
return {
|
||||||
|
shape,
|
||||||
|
bounds: shapeUtils[shape.type].getBounds(shape),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ShapeType.Circle: {
|
||||||
|
return {
|
||||||
|
shape,
|
||||||
|
bounds: shapeUtils[shape.type].getBounds(shape),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ShapeType.Rectangle: {
|
||||||
|
return {
|
||||||
|
shape,
|
||||||
|
bounds: shapeUtils[shape.type].getBounds(shape),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
|
@ -35,7 +35,10 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
brushSelecting: {
|
brushSelecting: {
|
||||||
onEnter: "startBrushSession",
|
onEnter: [
|
||||||
|
{ unless: "isPressingShiftKey", do: "clearSelection" },
|
||||||
|
"startBrushSession",
|
||||||
|
],
|
||||||
on: {
|
on: {
|
||||||
MOVED_POINTER: "updateBrushSession",
|
MOVED_POINTER: "updateBrushSession",
|
||||||
PANNED_CAMERA: "updateBrushSession",
|
PANNED_CAMERA: "updateBrushSession",
|
||||||
|
@ -44,6 +47,11 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
conditions: {
|
||||||
|
isPressingShiftKey(data, payload: { shiftKey: boolean }) {
|
||||||
|
return payload.shiftKey
|
||||||
|
},
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
cancelSession(data) {
|
cancelSession(data) {
|
||||||
session.cancel(data)
|
session.cancel(data)
|
||||||
|
@ -62,6 +70,11 @@ const state = createState({
|
||||||
updateBrushSession(data, payload: { point: number[] }) {
|
updateBrushSession(data, payload: { point: number[] }) {
|
||||||
session.update(data, screenToWorld(payload.point, data))
|
session.update(data, screenToWorld(payload.point, data))
|
||||||
},
|
},
|
||||||
|
// Selection
|
||||||
|
clearSelection(data) {
|
||||||
|
data.selectedIds = []
|
||||||
|
},
|
||||||
|
// Camera
|
||||||
zoomCamera(data, payload: { delta: number; point: number[] }) {
|
zoomCamera(data, payload: { delta: number; point: number[] }) {
|
||||||
const { camera } = data
|
const { camera } = data
|
||||||
const p0 = screenToWorld(payload.point, data)
|
const p0 = screenToWorld(payload.point, data)
|
||||||
|
@ -81,6 +94,11 @@ const state = createState({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
values: {
|
||||||
|
selectedIds(data) {
|
||||||
|
return new Set(data.selectedIds)
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
let session: Sessions.BaseSession
|
let session: Sessions.BaseSession
|
||||||
|
|
27
types.ts
27
types.ts
|
@ -21,18 +21,17 @@ export interface Page {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ShapeType {
|
export enum ShapeType {
|
||||||
|
Dot = "dot",
|
||||||
Circle = "circle",
|
Circle = "circle",
|
||||||
Ellipse = "ellipse",
|
Ellipse = "ellipse",
|
||||||
Square = "square",
|
|
||||||
Rectangle = "rectangle",
|
|
||||||
Line = "line",
|
Line = "line",
|
||||||
LineSegment = "lineSegment",
|
|
||||||
Dot = "dot",
|
|
||||||
Ray = "ray",
|
Ray = "ray",
|
||||||
Glob = "glob",
|
LineSegment = "lineSegment",
|
||||||
Spline = "spline",
|
Rectangle = "rectangle",
|
||||||
Cubic = "cubic",
|
// Glob = "glob",
|
||||||
Conic = "conic",
|
// Spline = "spline",
|
||||||
|
// Cubic = "cubic",
|
||||||
|
// Conic = "conic",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BaseShape {
|
export interface BaseShape {
|
||||||
|
@ -87,9 +86,9 @@ export interface RectangleShape extends BaseShape {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Shape =
|
export type Shape =
|
||||||
|
| DotShape
|
||||||
| CircleShape
|
| CircleShape
|
||||||
| EllipseShape
|
| EllipseShape
|
||||||
| DotShape
|
|
||||||
| LineShape
|
| LineShape
|
||||||
| RayShape
|
| RayShape
|
||||||
| LineSegmentShape
|
| LineSegmentShape
|
||||||
|
@ -103,3 +102,13 @@ export interface Bounds {
|
||||||
width: number
|
width: number
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Shapes extends Record<ShapeType, Shape> {
|
||||||
|
[ShapeType.Dot]: DotShape
|
||||||
|
[ShapeType.Circle]: CircleShape
|
||||||
|
[ShapeType.Ellipse]: EllipseShape
|
||||||
|
[ShapeType.Line]: LineShape
|
||||||
|
[ShapeType.Ray]: RayShape
|
||||||
|
[ShapeType.LineSegment]: LineSegmentShape
|
||||||
|
[ShapeType.Rectangle]: RectangleShape
|
||||||
|
}
|
||||||
|
|
103
utils/intersections.ts
Normal file
103
utils/intersections.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import { Bounds } from "types"
|
||||||
|
import * as vec from "utils/vec"
|
||||||
|
|
||||||
|
interface Intersection {
|
||||||
|
didIntersect: boolean
|
||||||
|
message: string
|
||||||
|
points: number[][]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intersectCircleLine(
|
||||||
|
c: number[],
|
||||||
|
r: number,
|
||||||
|
a1: number[],
|
||||||
|
a2: number[]
|
||||||
|
): Intersection {
|
||||||
|
const a =
|
||||||
|
(a2[0] - a1[0]) * (a2[0] - a1[0]) + (a2[1] - a1[1]) * (a2[1] - a1[1])
|
||||||
|
const b =
|
||||||
|
2 * ((a2[0] - a1[0]) * (a1[0] - c[0]) + (a2[1] - a1[1]) * (a1[1] - c[1]))
|
||||||
|
const cc =
|
||||||
|
c[0] * c[0] +
|
||||||
|
c[1] * c[1] +
|
||||||
|
a1[0] * a1[0] +
|
||||||
|
a1[1] * a1[1] -
|
||||||
|
2 * (c[0] * a1[0] + c[1] * a1[1]) -
|
||||||
|
r * r
|
||||||
|
|
||||||
|
const deter = b * b - 4 * a * cc
|
||||||
|
|
||||||
|
if (deter < 0) {
|
||||||
|
return { didIntersect: false, message: "outside", points: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deter === 0) {
|
||||||
|
return { didIntersect: false, message: "tangent", points: [] }
|
||||||
|
}
|
||||||
|
|
||||||
|
var e = Math.sqrt(deter)
|
||||||
|
var u1 = (-b + e) / (2 * a)
|
||||||
|
var u2 = (-b - e) / (2 * a)
|
||||||
|
if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
|
||||||
|
if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
|
||||||
|
return { didIntersect: false, message: "outside", points: [] }
|
||||||
|
} else {
|
||||||
|
return { didIntersect: false, message: "inside", points: [] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { didIntersect: true, message: "intersection", points: [] }
|
||||||
|
if (0 <= u1 && u1 <= 1) result.points.push(vec.lrp(a1, a2, u1))
|
||||||
|
if (0 <= u2 && u2 <= 1) result.points.push(vec.lrp(a1, a2, u2))
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intersectCircleRectangle(
|
||||||
|
c: number[],
|
||||||
|
r: number,
|
||||||
|
point: number[],
|
||||||
|
size: number[]
|
||||||
|
): Intersection[] {
|
||||||
|
const tl = point
|
||||||
|
const tr = vec.add(point, [size[0], 0])
|
||||||
|
const br = vec.add(point, size)
|
||||||
|
const bl = vec.add(point, [0, size[1]])
|
||||||
|
|
||||||
|
const intersections: Intersection[] = []
|
||||||
|
|
||||||
|
const topIntersection = intersectCircleLine(c, r, tl, tr)
|
||||||
|
if (topIntersection.didIntersect) {
|
||||||
|
intersections.push({ ...topIntersection, message: "top" })
|
||||||
|
}
|
||||||
|
const rightIntersection = intersectCircleLine(c, r, tr, br)
|
||||||
|
if (rightIntersection.didIntersect) {
|
||||||
|
intersections.push({ ...rightIntersection, message: "right" })
|
||||||
|
}
|
||||||
|
const bottomIntersection = intersectCircleLine(c, r, bl, br)
|
||||||
|
if (bottomIntersection.didIntersect) {
|
||||||
|
intersections.push({ ...bottomIntersection, message: "bottom" })
|
||||||
|
}
|
||||||
|
const leftIntersection = intersectCircleLine(c, r, tl, bl)
|
||||||
|
if (leftIntersection.didIntersect) {
|
||||||
|
intersections.push({ ...leftIntersection, message: "left" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intersectCircleBounds(
|
||||||
|
c: number[],
|
||||||
|
r: number,
|
||||||
|
bounds: Bounds
|
||||||
|
): Intersection[] {
|
||||||
|
const { minX, minY, width, height } = bounds
|
||||||
|
const intersections = intersectCircleRectangle(
|
||||||
|
c,
|
||||||
|
r,
|
||||||
|
[minX, minY],
|
||||||
|
[width, height]
|
||||||
|
)
|
||||||
|
|
||||||
|
return intersections
|
||||||
|
}
|
310
utils/shapes.ts
Normal file
310
utils/shapes.ts
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
import {
|
||||||
|
boundsCollide,
|
||||||
|
boundsContain,
|
||||||
|
pointInBounds,
|
||||||
|
} from "state/sessions/brush-session"
|
||||||
|
import {
|
||||||
|
Shape,
|
||||||
|
Bounds,
|
||||||
|
ShapeType,
|
||||||
|
CircleShape,
|
||||||
|
DotShape,
|
||||||
|
RectangleShape,
|
||||||
|
Shapes,
|
||||||
|
EllipseShape,
|
||||||
|
LineShape,
|
||||||
|
RayShape,
|
||||||
|
LineSegmentShape,
|
||||||
|
} from "types"
|
||||||
|
import { intersectCircleBounds } from "./intersections"
|
||||||
|
import * as vec from "./vec"
|
||||||
|
|
||||||
|
type BaseShapeUtils<K extends ShapeType> = {
|
||||||
|
getBounds(shape: Shapes[K]): Bounds
|
||||||
|
hitTest(shape: Shapes[K], test: number[] | Bounds): boolean
|
||||||
|
rotate(shape: Shapes[K]): Shapes[K]
|
||||||
|
translate(shape: Shapes[K]): Shapes[K]
|
||||||
|
scale(shape: Shapes[K], scale: number): Shapes[K]
|
||||||
|
stretch(shape: Shapes[K], scaleX: number, scaleY: number): Shapes[K]
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------- Dot ---------------------- */
|
||||||
|
|
||||||
|
const DotUtils: BaseShapeUtils<ShapeType.Dot> = {
|
||||||
|
getBounds(shape) {
|
||||||
|
const {
|
||||||
|
point: [cx, cy],
|
||||||
|
} = shape
|
||||||
|
|
||||||
|
return {
|
||||||
|
minX: cx - 2,
|
||||||
|
maxX: cx + 2,
|
||||||
|
minY: cy - 2,
|
||||||
|
maxY: cy + 2,
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hitTest(shape, test) {
|
||||||
|
if ("minX" in test) {
|
||||||
|
return pointInBounds(shape.point, test)
|
||||||
|
}
|
||||||
|
return vec.dist(shape.point, test) < 4
|
||||||
|
},
|
||||||
|
|
||||||
|
rotate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
translate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
scale(shape, scale: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
stretch(shape, scaleX: number, scaleY: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------- Circle --------------------- */
|
||||||
|
|
||||||
|
const CircleUtils: BaseShapeUtils<ShapeType.Circle> = {
|
||||||
|
getBounds(shape) {
|
||||||
|
const {
|
||||||
|
point: [cx, cy],
|
||||||
|
radius,
|
||||||
|
} = shape
|
||||||
|
|
||||||
|
return {
|
||||||
|
minX: cx - radius,
|
||||||
|
maxX: cx + radius,
|
||||||
|
minY: cy - radius,
|
||||||
|
maxY: cy + radius,
|
||||||
|
width: radius * 2,
|
||||||
|
height: radius * 2,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hitTest(shape, test) {
|
||||||
|
if ("minX" in test) {
|
||||||
|
const bounds = CircleUtils.getBounds(shape)
|
||||||
|
return (
|
||||||
|
boundsContain(bounds, test) ||
|
||||||
|
intersectCircleBounds(shape.point, shape.radius, bounds).length > 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return vec.dist(shape.point, test) < 4
|
||||||
|
},
|
||||||
|
|
||||||
|
rotate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
translate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
scale(shape, scale: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
stretch(shape, scaleX: number, scaleY: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --------------------- Ellipse -------------------- */
|
||||||
|
|
||||||
|
const EllipseUtils: BaseShapeUtils<ShapeType.Ellipse> = {
|
||||||
|
getBounds(shape) {
|
||||||
|
return {
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: 0,
|
||||||
|
maxY: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hitTest(shape) {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
rotate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
translate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
scale(shape, scale: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
stretch(shape, scaleX: number, scaleY: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------- Line ---------------------- */
|
||||||
|
|
||||||
|
const LineUtils: BaseShapeUtils<ShapeType.Line> = {
|
||||||
|
getBounds(shape) {
|
||||||
|
return {
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: 0,
|
||||||
|
maxY: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hitTest(shape) {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
rotate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
translate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
scale(shape, scale: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
stretch(shape, scaleX: number, scaleY: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ----------------------- Ray ---------------------- */
|
||||||
|
|
||||||
|
const RayUtils: BaseShapeUtils<ShapeType.Ray> = {
|
||||||
|
getBounds(shape) {
|
||||||
|
return {
|
||||||
|
minX: Infinity,
|
||||||
|
minY: Infinity,
|
||||||
|
maxX: Infinity,
|
||||||
|
maxY: Infinity,
|
||||||
|
width: Infinity,
|
||||||
|
height: Infinity,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hitTest(shape) {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
rotate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
translate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
scale(shape, scale: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
stretch(shape, scaleX: number, scaleY: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------ Line Segment ------------------ */
|
||||||
|
|
||||||
|
const LineSegmentUtils: BaseShapeUtils<ShapeType.LineSegment> = {
|
||||||
|
getBounds(shape) {
|
||||||
|
return {
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: 0,
|
||||||
|
maxY: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hitTest(shape) {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
rotate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
translate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
scale(shape, scale: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
stretch(shape, scaleX: number, scaleY: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------- Rectangle ------------------- */
|
||||||
|
|
||||||
|
const RectangleUtils: BaseShapeUtils<ShapeType.Rectangle> = {
|
||||||
|
getBounds(shape) {
|
||||||
|
const {
|
||||||
|
point: [x, y],
|
||||||
|
size: [width, height],
|
||||||
|
} = shape
|
||||||
|
|
||||||
|
return {
|
||||||
|
minX: x,
|
||||||
|
maxX: x + width,
|
||||||
|
minY: y,
|
||||||
|
maxY: y + height,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hitTest(shape) {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
rotate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
translate(shape) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
scale(shape, scale: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
|
||||||
|
stretch(shape, scaleX: number, scaleY: number) {
|
||||||
|
return shape
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const shapeUtils: { [K in ShapeType]: BaseShapeUtils<K> } = {
|
||||||
|
[ShapeType.Dot]: DotUtils,
|
||||||
|
[ShapeType.Circle]: CircleUtils,
|
||||||
|
[ShapeType.Ellipse]: EllipseUtils,
|
||||||
|
[ShapeType.Line]: LineUtils,
|
||||||
|
[ShapeType.Ray]: RayUtils,
|
||||||
|
[ShapeType.LineSegment]: LineSegmentUtils,
|
||||||
|
[ShapeType.Rectangle]: RectangleUtils,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default shapeUtils
|
Loading…
Reference in a new issue