finishes rotation

This commit is contained in:
Steve Ruiz 2021-05-18 09:32:20 +01:00
parent e2aac4b267
commit 1ece606db0
18 changed files with 353 additions and 198 deletions

View file

@ -6,10 +6,14 @@ import styled from "styles"
export default function BoundsBg() { export default function BoundsBg() {
const rBounds = useRef<SVGRectElement>(null) const rBounds = useRef<SVGRectElement>(null)
const bounds = useSelector((state) => state.values.selectedBounds) const bounds = useSelector((state) => state.values.selectedBounds)
const singleSelection = useSelector((s) => {
const rotation = useSelector((s) => {
if (s.data.selectedIds.size === 1) { if (s.data.selectedIds.size === 1) {
const { shapes } = s.data.document.pages[s.data.currentPageId]
const selected = Array.from(s.data.selectedIds.values())[0] const selected = Array.from(s.data.selectedIds.values())[0]
return s.data.document.pages[s.data.currentPageId].shapes[selected] return shapes[selected].rotation
} else {
return 0
} }
}) })
@ -30,12 +34,9 @@ export default function BoundsBg() {
rBounds.current.setPointerCapture(e.pointerId) rBounds.current.setPointerCapture(e.pointerId)
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds")) state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
}} }}
transform={ transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
singleSelection && minY + height / 2
`rotate(${singleSelection.rotation * (180 / Math.PI)},${ })`}
minX + width / 2
}, ${minY + width / 2})`
}
/> />
) )
} }

View file

@ -6,15 +6,19 @@ import { TransformCorner, TransformEdge } from "types"
import { lerp } from "utils/utils" import { lerp } from "utils/utils"
export default function Bounds() { export default function Bounds() {
const zoom = useSelector((state) => state.data.camera.zoom) const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
const bounds = useSelector((state) => state.values.selectedBounds) const zoom = useSelector((s) => s.data.camera.zoom)
const singleSelection = useSelector((s) => { const bounds = useSelector((s) => s.values.selectedBounds)
const rotation = useSelector((s) => {
if (s.data.selectedIds.size === 1) { if (s.data.selectedIds.size === 1) {
const { shapes } = s.data.document.pages[s.data.currentPageId]
const selected = Array.from(s.data.selectedIds.values())[0] const selected = Array.from(s.data.selectedIds.values())[0]
return s.data.document.pages[s.data.currentPageId].shapes[selected] return shapes[selected].rotation
} else {
return 0
} }
}) })
const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
if (!bounds) return null if (!bounds) return null
@ -26,12 +30,9 @@ export default function Bounds() {
return ( return (
<g <g
pointerEvents={isBrushing ? "none" : "all"} pointerEvents={isBrushing ? "none" : "all"}
transform={ transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
singleSelection && minY + height / 2
`rotate(${singleSelection.rotation * (180 / Math.PI)},${ })`}
minX + width / 2
}, ${minY + width / 2})`
}
> >
<StyledBounds <StyledBounds
x={minX} x={minX}

View file

@ -5,6 +5,7 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds" import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections" import { intersectCircleBounds } from "utils/intersections"
import { pointInCircle } from "utils/hitTests" import { pointInCircle } from "utils/hitTests"
import { translateBounds } from "utils/utils"
const circle = createShape<CircleShape>({ const circle = createShape<CircleShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -33,27 +34,26 @@ const circle = createShape<CircleShape>({
}, },
getBounds(shape) { getBounds(shape) {
if (this.boundsCache.has(shape)) { if (!this.boundsCache.has(shape)) {
return this.boundsCache.get(shape) const { radius } = shape
}
const {
point: [x, y],
radius,
} = shape
const bounds = { const bounds = {
minX: x, minX: 0,
maxX: x + radius * 2, maxX: radius * 2,
minY: y, minY: 0,
maxY: y + radius * 2, maxY: radius * 2,
width: radius * 2, width: radius * 2,
height: radius * 2, height: radius * 2,
} }
this.boundsCache.set(shape, bounds) this.boundsCache.set(shape, bounds)
}
return bounds return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
return this.getBounds(shape)
}, },
getCenter(shape) { getCenter(shape) {

View file

@ -6,6 +6,7 @@ import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections" import { intersectCircleBounds } from "utils/intersections"
import styled from "styles" import styled from "styles"
import { DotCircle } from "components/canvas/misc" import { DotCircle } from "components/canvas/misc"
import { translateBounds } from "utils/utils"
const dot = createShape<DotShape>({ const dot = createShape<DotShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -33,26 +34,24 @@ const dot = createShape<DotShape>({
}, },
getBounds(shape) { getBounds(shape) {
if (this.boundsCache.has(shape)) { if (!this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
}
const {
point: [x, y],
} = shape
const bounds = { const bounds = {
minX: x, minX: 0,
maxX: x + 1, maxX: 1,
minY: y, minY: 0,
maxY: y + 1, maxY: 1,
width: 1, width: 1,
height: 1, height: 1,
} }
this.boundsCache.set(shape, bounds) this.boundsCache.set(shape, bounds)
}
return bounds return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
return this.getBounds(shape)
}, },
getCenter(shape) { getCenter(shape) {

View file

@ -5,6 +5,7 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds" import { boundsContained } from "utils/bounds"
import { intersectEllipseBounds } from "utils/intersections" import { intersectEllipseBounds } from "utils/intersections"
import { pointInEllipse } from "utils/hitTests" import { pointInEllipse } from "utils/hitTests"
import { translateBounds } from "utils/utils"
const ellipse = createShape<EllipseShape>({ const ellipse = createShape<EllipseShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -36,28 +37,26 @@ const ellipse = createShape<EllipseShape>({
}, },
getBounds(shape) { getBounds(shape) {
if (this.boundsCache.has(shape)) { if (!this.boundsCache.has(shape)) {
return this.boundsCache.get(shape) const { radiusX, radiusY } = shape
}
const {
point: [x, y],
radiusX,
radiusY,
} = shape
const bounds = { const bounds = {
minX: x, minX: 0,
maxX: x + radiusX * 2, maxX: radiusX * 2,
minY: y, minY: 0,
maxY: y + radiusY * 2, maxY: radiusY * 2,
width: radiusX * 2, width: radiusX * 2,
height: radiusY * 2, height: radiusY * 2,
} }
this.boundsCache.set(shape, bounds) this.boundsCache.set(shape, bounds)
}
return bounds return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
return this.getBounds(shape)
}, },
getCenter(shape) { getCenter(shape) {

View file

@ -36,6 +36,9 @@ export interface ShapeUtility<K extends Shape> {
// Get the bounds of the a shape. // Get the bounds of the a shape.
getBounds(this: ShapeUtility<K>, shape: K): Bounds 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 // Get the center of the shape
getCenter(this: ShapeUtility<K>, shape: K): number[] getCenter(this: ShapeUtility<K>, shape: K): number[]

View file

@ -5,6 +5,7 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds" import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections" import { intersectCircleBounds } from "utils/intersections"
import { DotCircle } from "components/canvas/misc" import { DotCircle } from "components/canvas/misc"
import { translateBounds } from "utils/utils"
const line = createShape<LineShape>({ const line = createShape<LineShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -41,26 +42,24 @@ const line = createShape<LineShape>({
}, },
getBounds(shape) { getBounds(shape) {
if (this.boundsCache.has(shape)) { if (!this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
}
const {
point: [x, y],
} = shape
const bounds = { const bounds = {
minX: x, minX: 0,
maxX: x + 1, maxX: 1,
minY: y, minY: 0,
maxY: y + 1, maxY: 1,
width: 1, width: 1,
height: 1, height: 1,
} }
this.boundsCache.set(shape, bounds) this.boundsCache.set(shape, bounds)
}
return bounds return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
return this.getBounds(shape)
}, },
getCenter(shape) { getCenter(shape) {

View file

@ -3,7 +3,12 @@ import * as vec from "utils/vec"
import { PolylineShape, ShapeType } from "types" import { PolylineShape, ShapeType } from "types"
import { createShape } from "./index" import { createShape } from "./index"
import { intersectPolylineBounds } from "utils/intersections" import { intersectPolylineBounds } from "utils/intersections"
import { boundsCollide, boundsContained } from "utils/bounds" import {
boundsCollide,
boundsContained,
boundsContainPolygon,
} from "utils/bounds"
import { getBoundsFromPoints, translateBounds } from "utils/utils"
const polyline = createShape<PolylineShape>({ const polyline = createShape<PolylineShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -29,33 +34,16 @@ const polyline = createShape<PolylineShape>({
}, },
getBounds(shape) { getBounds(shape) {
if (this.boundsCache.has(shape)) { if (!this.boundsCache.has(shape)) {
return this.boundsCache.get(shape) const bounds = getBoundsFromPoints(shape.points)
}
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)
}
const bounds = {
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,
}
this.boundsCache.set(shape, bounds) this.boundsCache.set(shape, bounds)
return bounds }
return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
return this.getBounds(shape)
}, },
getCenter(shape) { getCenter(shape) {
@ -78,15 +66,23 @@ const polyline = createShape<PolylineShape>({
return false return false
}, },
hitTestBounds(this, shape, bounds) { hitTestBounds(this, shape, brushBounds) {
const shapeBounds = this.getBounds(shape) 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 ( return (
boundsContained(shapeBounds, bounds) || boundsContainPolygon(brushBounds, rotatedCorners) ||
(boundsCollide(shapeBounds, bounds) &&
intersectPolylineBounds( intersectPolylineBounds(
shape.points.map((point) => vec.add(point, shape.point)), shape.points.map((point) => vec.add(point, shape.point)),
bounds brushBounds
).length > 0) ).length > 0
) )
}, },

View file

@ -5,6 +5,7 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds" import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections" import { intersectCircleBounds } from "utils/intersections"
import { DotCircle } from "components/canvas/misc" import { DotCircle } from "components/canvas/misc"
import { translateBounds } from "utils/utils"
const ray = createShape<RayShape>({ const ray = createShape<RayShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -40,27 +41,25 @@ const ray = createShape<RayShape>({
) )
}, },
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getBounds(shape) { getBounds(shape) {
if (this.boundsCache.has(shape)) { if (!this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
}
const {
point: [x, y],
} = shape
const bounds = { const bounds = {
minX: x, minX: 0,
maxX: x + 8, maxX: 1,
minY: y, minY: 0,
maxY: y + 8, maxY: 1,
width: 8, width: 1,
height: 8, height: 1,
} }
this.boundsCache.set(shape, bounds) this.boundsCache.set(shape, bounds)
}
return bounds return translateBounds(this.boundsCache.get(shape), shape.point)
}, },
getCenter(shape) { getCenter(shape) {

View file

@ -2,7 +2,8 @@ import { v4 as uuid } from "uuid"
import * as vec from "utils/vec" import * as vec from "utils/vec"
import { RectangleShape, ShapeType } from "types" import { RectangleShape, ShapeType } from "types"
import { createShape } from "./index" import { createShape } from "./index"
import { boundsContained, boundsCollide } from "utils/bounds" import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
import { getBoundsFromPoints, rotateBounds, translateBounds } from "utils/utils"
const rectangle = createShape<RectangleShape>({ const rectangle = createShape<RectangleShape>({
boundsCache: new WeakMap([]), boundsCache: new WeakMap([]),
@ -31,31 +32,40 @@ const rectangle = createShape<RectangleShape>({
}, },
getBounds(shape) { getBounds(shape) {
if (this.boundsCache.has(shape)) { if (!this.boundsCache.has(shape)) {
return this.boundsCache.get(shape) const [width, height] = shape.size
}
const {
point: [x, y],
size: [width, height],
} = shape
const bounds = { const bounds = {
minX: x, minX: 0,
maxX: x + width, maxX: width,
minY: y, minY: 0,
maxY: y + height, maxY: height,
width, width,
height, height,
} }
this.boundsCache.set(shape, bounds) this.boundsCache.set(shape, bounds)
}
return bounds return translateBounds(this.boundsCache.get(shape), shape.point)
},
getRotatedBounds(shape) {
const b = this.getBounds(shape)
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
// Rotate corners of the shape, then find the minimum among those points.
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 getBoundsFromPoints(rotatedCorners)
}, },
getCenter(shape) { getCenter(shape) {
const bounds = this.getBounds(shape) const bounds = this.getRotatedBounds(shape)
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2] return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
}, },
@ -64,10 +74,19 @@ const rectangle = createShape<RectangleShape>({
}, },
hitTestBounds(shape, brushBounds) { hitTestBounds(shape, brushBounds) {
const shapeBounds = this.getBounds(shape) 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 ( return (
boundsContained(shapeBounds, brushBounds) || boundsContainPolygon(brushBounds, rotatedCorners) ||
boundsCollide(shapeBounds, brushBounds) boundsCollidePolygon(brushBounds, rotatedCorners)
) )
}, },

View file

@ -16,16 +16,24 @@ export default function translateCommand(
do(data) { do(data) {
const { shapes } = data.document.pages[after.currentPageId] const { shapes } = data.document.pages[after.currentPageId]
for (let { id, rotation } of after.shapes) { for (let { id, point, rotation } of after.shapes) {
shapes[id].rotation = rotation const shape = shapes[id]
shape.rotation = rotation
shape.point = point
} }
data.boundsRotation = after.boundsRotation
}, },
undo(data) { undo(data) {
const { shapes } = data.document.pages[before.currentPageId] const { shapes } = data.document.pages[before.currentPageId]
for (let { id, rotation } of before.shapes) { for (let { id, point, rotation } of before.shapes) {
shapes[id].rotation = rotation const shape = shapes[id]
shape.rotation = rotation
shape.point = point
} }
data.boundsRotation = before.boundsRotation
}, },
}) })
) )

View file

@ -25,7 +25,7 @@ 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 brushBounds = getBoundsFromPoints(origin, point) const brushBounds = getBoundsFromPoints([origin, point])
for (let { test, id } of snapshot.shapes) { for (let { test, id } of snapshot.shapes) {
if (test(brushBounds)) { if (test(brushBounds)) {

View file

@ -18,24 +18,34 @@ export default class RotateSession extends BaseSession {
} }
update(data: Data, point: number[]) { update(data: Data, point: number[]) {
const { currentPageId, center, shapes } = this.snapshot const { currentPageId, boundsCenter, shapes } = this.snapshot
const { document } = data const { document } = data
const a1 = vec.angle(center, this.origin) const a1 = vec.angle(boundsCenter, this.origin)
const a2 = vec.angle(center, point) const a2 = vec.angle(boundsCenter, point)
for (let { id, rotation } of shapes) { data.boundsRotation =
(this.snapshot.boundsRotation + (a2 - a1)) % (Math.PI * 2)
for (let { id, center, offset, rotation } of shapes) {
const shape = document.pages[currentPageId].shapes[id] const shape = document.pages[currentPageId].shapes[id]
shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2)) shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
const newCenter = vec.rotWith(
center,
boundsCenter,
(a2 - a1) % (Math.PI * 2)
)
shape.point = vec.sub(newCenter, offset)
} }
} }
cancel(data: Data) { cancel(data: Data) {
const { document } = data const { document } = data
for (let shape of this.snapshot.shapes) { for (let { id, point, rotation } of this.snapshot.shapes) {
document.pages[this.snapshot.currentPageId].shapes[shape.id].rotation = const shape = document.pages[this.snapshot.currentPageId].shapes[id]
shape.rotation shape.rotation = rotation
shape.point = point
} }
} }
@ -46,9 +56,10 @@ export default class RotateSession extends BaseSession {
export function getRotateSnapshot(data: Data) { export function getRotateSnapshot(data: Data) {
const { const {
boundsRotation,
selectedIds, selectedIds,
document: { pages },
currentPageId, currentPageId,
document: { pages },
} = current(data) } = current(data)
const shapes = Array.from(selectedIds.values()).map( const shapes = Array.from(selectedIds.values()).map(
@ -63,18 +74,28 @@ export function getRotateSnapshot(data: Data) {
// The common (exterior) bounds of the selected shapes // The common (exterior) bounds of the selected shapes
const bounds = getCommonBounds(...Object.values(shapesBounds)) const bounds = getCommonBounds(...Object.values(shapesBounds))
const center = [ const boundsCenter = [
bounds.minX + bounds.width / 2, bounds.minX + bounds.width / 2,
bounds.minY + bounds.height / 2, bounds.minY + bounds.height / 2,
] ]
return { return {
currentPageId, currentPageId,
center, boundsCenter,
shapes: shapes.map(({ id, rotation }) => ({ boundsRotation,
shapes: shapes.map(({ id, point, rotation }) => {
const bounds = shapesBounds[id]
const offset = [bounds.width / 2, bounds.height / 2]
const center = vec.add(offset, [bounds.minX, bounds.minY])
return {
id, id,
point,
rotation, rotation,
})), offset,
center,
}
}),
} }
} }

View file

@ -29,6 +29,7 @@ const initialData: Data = {
zoom: 1, zoom: 1,
}, },
brush: undefined, brush: undefined,
boundsRotation: 0,
pointedId: null, pointedId: null,
hoveredId: null, hoveredId: null,
selectedIds: new Set([]), selectedIds: new Set([]),
@ -180,6 +181,7 @@ const state = createState({
brushSelecting: { brushSelecting: {
onEnter: [ onEnter: [
{ unless: "isPressingShiftKey", do: "clearSelectedIds" }, { unless: "isPressingShiftKey", do: "clearSelectedIds" },
"clearBoundsRotation",
"startBrushSession", "startBrushSession",
], ],
on: { on: {
@ -708,6 +710,10 @@ const state = createState({
restoreSavedData(data) { restoreSavedData(data) {
history.load(data) history.load(data)
}, },
clearBoundsRotation(data) {
data.boundsRotation = 0
},
}, },
values: { values: {
selectedIds(data) { selectedIds(data) {
@ -726,12 +732,14 @@ const state = createState({
if (selectedIds.size === 0) return null if (selectedIds.size === 0) return null
if (selectedIds.size === 1 && !getShapeUtils(shapes[0]).canTransform) { if (selectedIds.size === 1) {
return null const shapeUtils = getShapeUtils(shapes[0])
if (!shapeUtils.canTransform) return null
return shapeUtils.getBounds(shapes[0])
} }
return getCommonBounds( return getCommonBounds(
...shapes.map((shape) => getShapeUtils(shape).getBounds(shape)) ...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
) )
}, },
}, },

View file

@ -18,6 +18,7 @@ export interface Data {
zoom: number zoom: number
} }
brush?: Bounds brush?: Bounds
boundsRotation: number
selectedIds: Set<string> selectedIds: Set<string>
pointedId?: string pointedId?: string
hoveredId?: string hoveredId?: string
@ -168,6 +169,10 @@ export interface Bounds {
height: number height: number
} }
export interface RotatedBounds extends Bounds {
rotation: number
}
export interface ShapeBounds extends Bounds { export interface ShapeBounds extends Bounds {
id: string id: string
} }

View file

@ -1,4 +1,8 @@
import { Bounds } from "types" import { Bounds } from "types"
import {
intersectPolygonBounds,
intersectPolylineBounds,
} from "./intersections"
/** /**
* Get whether two bounds collide. * Get whether two bounds collide.
@ -37,6 +41,23 @@ export function boundsContained(a: Bounds, b: Bounds) {
return boundsContain(b, a) return boundsContain(b, a)
} }
/**
* Get whether a set of points are all contained by a bounding box.
* @returns
*/
export function boundsContainPolygon(a: Bounds, points: number[][]) {
return points.every((point) => pointInBounds(point, a))
}
/**
* Get whether a polygon collides a bounding box.
* @param points
* @param b
*/
export function boundsCollidePolygon(a: Bounds, points: number[][]) {
return intersectPolygonBounds(points, a).length > 0
}
/** /**
* Get whether two bounds are identical. * Get whether two bounds are identical.
* @param a Bounds * @param a Bounds

View file

@ -342,3 +342,21 @@ export function intersectPolylineBounds(points: number[][], bounds: Bounds) {
return intersections return intersections
} }
export function intersectPolygonBounds(points: number[][], bounds: Bounds) {
const { minX, minY, width, height } = bounds
const intersections: Intersection[] = []
for (let i = 1; i < points.length + 1; i++) {
intersections.push(
...intersectRectangleLineSegment(
[minX, minY],
[width, height],
points[i - 1],
points[i % points.length]
)
)
}
return intersections
}

View file

@ -41,21 +41,21 @@ export function getCommonBounds(...b: Bounds[]) {
return bounds return bounds
} }
export function getBoundsFromPoints(a: number[], b: number[]) { // export function getBoundsFromPoints(a: number[], b: number[]) {
const minX = Math.min(a[0], b[0]) // const minX = Math.min(a[0], b[0])
const maxX = Math.max(a[0], b[0]) // const maxX = Math.max(a[0], b[0])
const minY = Math.min(a[1], b[1]) // const minY = Math.min(a[1], b[1])
const maxY = Math.max(a[1], b[1]) // const maxY = Math.max(a[1], b[1])
return { // return {
minX, // minX,
maxX, // maxX,
minY, // minY,
maxY, // maxY,
width: maxX - minX, // width: maxX - minX,
height: maxY - minY, // height: maxY - minY,
} // }
} // }
// A helper for getting tangents. // A helper for getting tangents.
export function getCircleTangentToPoint( export function getCircleTangentToPoint(
@ -962,3 +962,61 @@ export function vectorToPoint(point: number[] | Vector | undefined) {
} }
return point return point
} }
export function getBoundsFromPoints(points: number[][]): Bounds {
let minX = Infinity
let minY = Infinity
let maxX = -Infinity
let maxY = -Infinity
for (let [x, y] of points) {
minX = Math.min(x, minX)
minY = Math.min(y, minY)
maxX = Math.max(x, maxX)
maxY = Math.max(y, maxY)
}
return {
minX,
minY,
maxX,
maxY,
width: maxX - minX,
height: maxY - minY,
}
}
/**
* Move a bounding box without recalculating it.
* @param bounds
* @param delta
* @returns
*/
export function translateBounds(bounds: Bounds, delta: number[]) {
return {
minX: bounds.minX + delta[0],
minY: bounds.minY + delta[1],
maxX: bounds.maxX + delta[0],
maxY: bounds.maxY + delta[1],
width: bounds.width,
height: bounds.height,
}
}
export function rotateBounds(
bounds: Bounds,
center: number[],
rotation: number
) {
const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
return {
minX,
minY,
maxX,
maxY,
width: bounds.width,
height: bounds.height,
}
}