adds polyline, intersections for polyline

This commit is contained in:
Steve Ruiz 2021-05-12 10:18:11 +01:00
parent 7ee3a1ef3d
commit f5d555863c
11 changed files with 254 additions and 128 deletions

View file

@ -3,6 +3,7 @@ import { useSelector } from "state"
import { ShapeType } from "types"
import Circle from "./shapes/circle"
import Dot from "./shapes/dot"
import Polyline from "./shapes/polyline"
import Rectangle from "./shapes/rectangle"
/*
@ -24,6 +25,8 @@ function Shape({ id }: { id: string }) {
return <Circle {...shape} />
case ShapeType.Rectangle:
return <Rectangle {...shape} />
case ShapeType.Polyline:
return <Polyline {...shape} />
default:
return null
}

View file

@ -1,10 +1,8 @@
import state, { useSelector } from "state"
import { useSelector } from "state"
import { CircleShape } from "types"
import ShapeGroup from "./shape-group"
import { getPointerEventInfo } from "utils/utils"
import ShapeGroup from "./shape-g"
interface BaseCircleProps {
point: number[]
interface BaseCircleProps extends Pick<CircleShape, "radius"> {
radius: number
fill?: string
stroke?: string
@ -12,7 +10,6 @@ interface BaseCircleProps {
}
function BaseCircle({
point,
radius,
fill = "#ccc",
stroke = "none",
@ -20,8 +17,8 @@ function BaseCircle({
}: BaseCircleProps) {
return (
<circle
cx={point[0] + strokeWidth}
cy={point[1] + strokeWidth}
cx={strokeWidth}
cy={strokeWidth}
r={radius - strokeWidth}
fill={fill}
stroke={stroke}
@ -33,16 +30,10 @@ function BaseCircle({
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} />
<ShapeGroup id={id} point={point}>
<BaseCircle radius={radius} />
{isSelected && (
<BaseCircle
point={point}
radius={radius}
fill="none"
stroke="blue"
strokeWidth={1}
/>
<BaseCircle radius={radius} fill="none" stroke="blue" strokeWidth={1} />
)}
</ShapeGroup>
)

View file

@ -1,50 +1,46 @@
import { useSelector } from "state"
import { DotShape } from "types"
import ShapeGroup from "./shape-group"
import ShapeGroup from "./shape-g"
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}
cx={strokeWidth}
cy={strokeWidth}
r={8}
fill="transparent"
stroke="none"
strokeWidth="0"
/>
<circle
cx={point[0] + strokeWidth}
cy={point[1] + strokeWidth}
cx={strokeWidth}
cy={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 id={id} point={point}>
<BaseDot />
{isSelected && <BaseDot fill="none" stroke="blue" strokeWidth={1} />}
</ShapeGroup>
)
}

View file

@ -0,0 +1,35 @@
import { useSelector } from "state"
import { PolylineShape } from "types"
import ShapeGroup from "./shape-g"
interface BasePolylineProps extends Pick<PolylineShape, "points"> {
fill?: string
stroke?: string
strokeWidth?: number
}
function BasePolyline({
points,
fill = "none",
stroke = "#ccc",
strokeWidth = 2,
}: BasePolylineProps) {
return (
<polyline
points={points.toString()}
fill={fill}
stroke={stroke}
strokeWidth={strokeWidth}
/>
)
}
export default function Polyline({ id, point, points }: PolylineShape) {
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
return (
<ShapeGroup id={id} point={point}>
<BasePolyline points={points} />
{isSelected && <BasePolyline points={points} fill="none" stroke="blue" />}
</ShapeGroup>
)
}

View file

@ -1,9 +1,8 @@
import { useSelector } from "state"
import { RectangleShape } from "types"
import ShapeGroup from "./shape-group"
import ShapeGroup from "./shape-g"
interface BaseRectangleProps {
point: number[]
interface BaseRectangleProps extends Pick<RectangleShape, "size"> {
size: number[]
fill?: string
stroke?: string
@ -11,7 +10,6 @@ interface BaseRectangleProps {
}
function BaseRectangle({
point,
size,
fill = "#ccc",
stroke = "none",
@ -19,8 +17,8 @@ function BaseRectangle({
}: BaseRectangleProps) {
return (
<rect
x={point[0] + strokeWidth}
y={point[1] + strokeWidth}
x={strokeWidth}
y={strokeWidth}
width={size[0] - strokeWidth * 2}
height={size[1] - strokeWidth * 2}
fill={fill}
@ -33,16 +31,10 @@ function BaseRectangle({
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} />
<ShapeGroup id={id} point={point}>
<BaseRectangle size={size} />
{isSelected && (
<BaseRectangle
point={point}
size={size}
fill="none"
stroke="blue"
strokeWidth={1}
/>
<BaseRectangle size={size} fill="none" stroke="blue" strokeWidth={1} />
)}
</ShapeGroup>
)

View file

@ -1,17 +1,19 @@
import React from "react"
import state from "state"
import { Shape } from "types"
import { getPointerEventInfo } from "utils/utils"
export default function ShapeGroup({
id,
children,
point,
}: {
id: string
children: React.ReactNode
point: number[]
}) {
return (
<g
transform={`translate(${point})`}
onPointerDown={(e) =>
state.send("POINTED_SHAPE", { id, ...getPointerEventInfo(e) })
}

View file

@ -30,12 +30,16 @@ export const defaultDocument: Data["document"] = {
},
shape2: {
id: "shape2",
type: ShapeType.Circle,
type: ShapeType.Polyline,
name: "Shape 2",
parentId: "page0",
childIndex: 2,
point: [200, 800],
radius: 25,
point: [200, 600],
points: [
[0, 0],
[75, 200],
[100, 50],
],
rotation: 0,
},
shape3: {

View file

@ -1,14 +1,17 @@
import { current } from "immer"
import { Bounds, Data, Shape, ShapeType } from "types"
import BaseSession from "./base-session"
import shapeUtils from "utils/shapes"
import shapeUtils from "utils/shape-utils"
import { getBoundsFromPoints } from "utils/utils"
import * as vec from "utils/vec"
import { intersectCircleBounds } from "utils/intersections"
import {
intersectCircleBounds,
intersectPolylineBounds,
} from "utils/intersections"
interface BrushSnapshot {
selectedIds: string[]
shapes: { shape: Shape; bounds: Bounds }[]
shapes: { shape: Shape; test: (bounds: Bounds) => boolean }[]
}
export default class BrushSession extends BaseSession {
@ -31,32 +34,7 @@ export default class BrushSession extends BaseSession {
data.selectedIds = [
...snapshot.selectedIds,
...snapshot.shapes
.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)
}
}
})
.filter(({ test }) => test(brushBounds))
.map(({ shape }) => shape.id),
]
@ -72,7 +50,7 @@ export default class BrushSession extends BaseSession {
data.brush = undefined
}
static getSnapshot(data: Data) {
static getSnapshot(data: Data): BrushSnapshot {
const {
selectedIds,
document: { pages },
@ -88,21 +66,47 @@ export default class BrushSession extends BaseSession {
.map((shape) => {
switch (shape.type) {
case ShapeType.Dot: {
const bounds = shapeUtils[shape.type].getBounds(shape)
return {
shape,
bounds: shapeUtils[shape.type].getBounds(shape),
test: (brushBounds: Bounds) =>
boundsContained(bounds, brushBounds) ||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0,
}
}
case ShapeType.Circle: {
const bounds = shapeUtils[shape.type].getBounds(shape)
return {
shape,
bounds: shapeUtils[shape.type].getBounds(shape),
test: (brushBounds: Bounds) =>
boundsContained(bounds, brushBounds) ||
intersectCircleBounds(shape.point, shape.radius, brushBounds)
.length > 0,
}
}
case ShapeType.Rectangle: {
const bounds = shapeUtils[shape.type].getBounds(shape)
return {
shape,
bounds: shapeUtils[shape.type].getBounds(shape),
test: (brushBounds: Bounds) =>
boundsContained(bounds, brushBounds) ||
boundsCollide(bounds, brushBounds),
}
}
case ShapeType.Polyline: {
const bounds = shapeUtils[shape.type].getBounds(shape)
const points = shape.points.map((point) =>
vec.add(point, shape.point)
)
return {
shape,
test: (brushBounds: Bounds) =>
boundsContained(bounds, brushBounds) ||
intersectPolylineBounds(points, brushBounds).length > 0,
}
}
default: {

View file

@ -26,7 +26,7 @@ export enum ShapeType {
Ellipse = "ellipse",
Line = "line",
Ray = "ray",
LineSegment = "lineSegment",
Polyline = "Polyline",
Rectangle = "rectangle",
// Glob = "glob",
// Spline = "spline",
@ -40,48 +40,42 @@ export interface BaseShape {
parentId: string
childIndex: number
name: string
point: number[]
rotation: 0
}
export interface DotShape extends BaseShape {
type: ShapeType.Dot
point: number[]
}
export interface CircleShape extends BaseShape {
type: ShapeType.Circle
point: number[]
radius: number
}
export interface EllipseShape extends BaseShape {
type: ShapeType.Ellipse
point: number[]
radiusX: number
radiusY: number
}
export interface LineShape extends BaseShape {
type: ShapeType.Line
point: number[]
vector: number[]
}
export interface RayShape extends BaseShape {
type: ShapeType.Ray
point: number[]
vector: number[]
}
export interface LineSegmentShape extends BaseShape {
type: ShapeType.LineSegment
start: number[]
end: number[]
export interface PolylineShape extends BaseShape {
type: ShapeType.Polyline
points: number[][]
}
export interface RectangleShape extends BaseShape {
type: ShapeType.Rectangle
point: number[]
size: number[]
}
@ -91,7 +85,7 @@ export type Shape =
| EllipseShape
| LineShape
| RayShape
| LineSegmentShape
| PolylineShape
| RectangleShape
export interface Bounds {
@ -109,6 +103,6 @@ export interface Shapes extends Record<ShapeType, Shape> {
[ShapeType.Ellipse]: EllipseShape
[ShapeType.Line]: LineShape
[ShapeType.Ray]: RayShape
[ShapeType.LineSegment]: LineSegmentShape
[ShapeType.Polyline]: PolylineShape
[ShapeType.Rectangle]: RectangleShape
}

View file

@ -7,7 +7,47 @@ interface Intersection {
points: number[][]
}
export function intersectCircleLine(
function getIntersection(
points: number[][],
message = points.length ? "Intersection" : "No intersection"
) {
return { didIntersect: points.length > 0, message, points }
}
export function intersectLineSegments(
a1: number[],
a2: number[],
b1: number[],
b2: number[]
) {
const AB = vec.sub(a1, b1)
const BV = vec.sub(b2, b1)
const AV = vec.sub(a2, a1)
const ua_t = BV[0] * AB[1] - BV[1] * AB[0]
const ub_t = AV[0] * AB[1] - AV[1] * AB[0]
const u_b = BV[1] * AV[0] - BV[0] * AV[1]
if (ua_t === 0 || ub_t === 0) {
return getIntersection([], "Coincident")
}
if (u_b === 0) {
return getIntersection([], "Parallel")
}
if (u_b != 0) {
const ua = ua_t / u_b
const ub = ub_t / u_b
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
return getIntersection([vec.add(a1, vec.mul(AV, ua))])
}
}
return getIntersection([])
}
export function intersectCircleLineSegment(
c: number[],
r: number,
a1: number[],
@ -66,19 +106,23 @@ export function intersectCircleRectangle(
const intersections: Intersection[] = []
const topIntersection = intersectCircleLine(c, r, tl, tr)
const topIntersection = intersectCircleLineSegment(c, r, tl, tr)
const rightIntersection = intersectCircleLineSegment(c, r, tr, br)
const bottomIntersection = intersectCircleLineSegment(c, r, bl, br)
const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
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" })
}
@ -86,18 +130,79 @@ export function intersectCircleRectangle(
return intersections
}
export function intersectRectangleLineSegment(
point: number[],
size: number[],
a1: number[],
a2: number[]
) {
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 = intersectLineSegments(a1, a2, tl, tr)
const rightIntersection = intersectLineSegments(a1, a2, tr, br)
const bottomIntersection = intersectLineSegments(a1, a2, bl, br)
const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
if (topIntersection.didIntersect) {
intersections.push({ ...topIntersection, message: "top" })
}
if (rightIntersection.didIntersect) {
intersections.push({ ...rightIntersection, message: "right" })
}
if (bottomIntersection.didIntersect) {
intersections.push({ ...bottomIntersection, message: "bottom" })
}
if (leftIntersection.didIntersect) {
intersections.push({ ...leftIntersection, message: "left" })
}
return intersections
}
/* -------------------------------------------------- */
/* Shape vs. Bounds */
/* -------------------------------------------------- */
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 intersectCircleRectangle(c, r, [minX, minY], [width, height])
}
export function intersectLineSegmentBounds(
a1: number[],
a2: number[],
bounds: Bounds
) {
const { minX, minY, width, height } = bounds
return intersectRectangleLineSegment([minX, minY], [width, height], a1, a2)
}
export function intersectPolylineBounds(points: number[][], bounds: Bounds) {
const { minX, minY, width, height } = bounds
const intersections: Intersection[] = []
for (let i = 1; i < points.length; i++) {
intersections.push(
...intersectRectangleLineSegment(
[minX, minY],
[width, height],
points[i - 1],
points[i]
)
)
}
return intersections
}

View file

@ -3,19 +3,7 @@ import {
boundsContain,
pointInBounds,
} from "state/sessions/brush-session"
import {
Shape,
Bounds,
ShapeType,
CircleShape,
DotShape,
RectangleShape,
Shapes,
EllipseShape,
LineShape,
RayShape,
LineSegmentShape,
} from "types"
import { Bounds, ShapeType, Shapes } from "types"
import { intersectCircleBounds } from "./intersections"
import * as vec from "./vec"
@ -224,15 +212,27 @@ const RayUtils: BaseShapeUtils<ShapeType.Ray> = {
/* ------------------ Line Segment ------------------ */
const LineSegmentUtils: BaseShapeUtils<ShapeType.LineSegment> = {
const PolylineUtils: BaseShapeUtils<ShapeType.Polyline> = {
getBounds(shape) {
let minX = 0
let minY = 0
let maxX = 0
let maxY = 0
for (let [x, y] of shape.points) {
minX = Math.min(x, minX)
minY = Math.min(y, minY)
maxX = Math.max(x, maxX)
maxY = Math.max(y, maxY)
}
return {
minX: 0,
minY: 0,
maxX: 0,
maxY: 0,
width: 0,
height: 0,
minX: minX + shape.point[0],
minY: minY + shape.point[1],
maxX: maxX + shape.point[0],
maxY: maxY + shape.point[1],
width: maxX - minX,
height: maxY - minY,
}
},
@ -303,7 +303,7 @@ const shapeUtils: { [K in ShapeType]: BaseShapeUtils<K> } = {
[ShapeType.Ellipse]: EllipseUtils,
[ShapeType.Line]: LineUtils,
[ShapeType.Ray]: RayUtils,
[ShapeType.LineSegment]: LineSegmentUtils,
[ShapeType.Polyline]: PolylineUtils,
[ShapeType.Rectangle]: RectangleUtils,
}