@zag-js/rect-utils 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -2
- package/src/align.ts +59 -0
- package/src/closest.ts +53 -0
- package/src/contains.ts +14 -0
- package/src/distance.ts +43 -0
- package/src/from-element.ts +59 -0
- package/src/from-points.ts +15 -0
- package/src/from-range.ts +26 -0
- package/src/from-rotation.ts +41 -0
- package/src/from-window.ts +33 -0
- package/src/get-polygon.ts +15 -0
- package/src/index.ts +16 -0
- package/src/intersection.ts +32 -0
- package/src/operations.ts +35 -0
- package/src/polygon.ts +53 -0
- package/src/rect.ts +56 -0
- package/src/types.ts +45 -0
- package/src/union.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zag-js/rect-utils",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"js",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"repository": "https://github.com/chakra-ui/zag/tree/main/packages/utilities/rect",
|
|
14
14
|
"sideEffects": false,
|
|
15
15
|
"files": [
|
|
16
|
-
"dist
|
|
16
|
+
"dist",
|
|
17
|
+
"src"
|
|
17
18
|
],
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
package/src/align.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Rect } from "./rect"
|
|
2
|
+
|
|
3
|
+
function hAlign(a: Rect, ref: Rect, h: HAlign): Rect {
|
|
4
|
+
let x = ref.minX
|
|
5
|
+
|
|
6
|
+
if (h === "left-inside") {
|
|
7
|
+
x = ref.minX
|
|
8
|
+
}
|
|
9
|
+
if (h === "left-outside") {
|
|
10
|
+
x = ref.minX - ref.width
|
|
11
|
+
}
|
|
12
|
+
if (h === "right-inside") {
|
|
13
|
+
x = ref.maxX - ref.width
|
|
14
|
+
}
|
|
15
|
+
if (h === "right-outside") {
|
|
16
|
+
x = ref.maxX
|
|
17
|
+
}
|
|
18
|
+
if (h === "center") {
|
|
19
|
+
x = ref.midX - ref.width / 2
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return { ...a, x }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function vAlign(a: Rect, ref: Rect, v: VAlign): Rect {
|
|
26
|
+
let y = ref.minY
|
|
27
|
+
|
|
28
|
+
if (v === "top-inside") {
|
|
29
|
+
y = ref.minY
|
|
30
|
+
}
|
|
31
|
+
if (v === "top-outside") {
|
|
32
|
+
y = ref.minY - a.height
|
|
33
|
+
}
|
|
34
|
+
if (v === "bottom-inside") {
|
|
35
|
+
y = ref.maxY - a.height
|
|
36
|
+
}
|
|
37
|
+
if (v === "bottom-outside") {
|
|
38
|
+
y = ref.maxY
|
|
39
|
+
}
|
|
40
|
+
if (v === "center") {
|
|
41
|
+
y = ref.midY - a.height / 2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { ...a, y }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function alignRect(a: Rect, ref: Rect, options: AlignOptions): Rect {
|
|
48
|
+
const { h, v } = options
|
|
49
|
+
return vAlign(hAlign(a, ref, h), ref, v)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type AlignOptions = {
|
|
53
|
+
h: HAlign
|
|
54
|
+
v: VAlign
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type HAlign = "left-inside" | "left-outside" | "center" | "right-inside" | "right-outside"
|
|
58
|
+
|
|
59
|
+
export type VAlign = "top-inside" | "top-outside" | "center" | "bottom-inside" | "bottom-outside"
|
package/src/closest.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { distance } from "./distance"
|
|
2
|
+
import type { Rect } from "./rect"
|
|
3
|
+
import type { Point, RectSide } from "./types"
|
|
4
|
+
|
|
5
|
+
export function closest(...pts: Point[]) {
|
|
6
|
+
return (a: Point): Point => {
|
|
7
|
+
const ds = pts.map((b) => distance(b, a))
|
|
8
|
+
const c = Math.min.apply(Math, ds)
|
|
9
|
+
return pts[ds.indexOf(c)]
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function closestSideToRect(ref: Rect, r: Rect): RectSide {
|
|
14
|
+
if (r.maxX <= ref.minX) {
|
|
15
|
+
return "left"
|
|
16
|
+
}
|
|
17
|
+
if (r.minX >= ref.maxX) {
|
|
18
|
+
return "right"
|
|
19
|
+
}
|
|
20
|
+
if (r.maxY <= ref.minY) {
|
|
21
|
+
return "top"
|
|
22
|
+
}
|
|
23
|
+
if (r.minY >= ref.maxY) {
|
|
24
|
+
return "bottom"
|
|
25
|
+
}
|
|
26
|
+
return "left"
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function closestSideToPoint(ref: Rect, p: Point): RectSide {
|
|
30
|
+
const { x, y } = p
|
|
31
|
+
|
|
32
|
+
const dl = x - ref.minX
|
|
33
|
+
const dr = ref.maxX - x
|
|
34
|
+
const dt = y - ref.minY
|
|
35
|
+
const db = ref.maxY - y
|
|
36
|
+
|
|
37
|
+
let closest = dl
|
|
38
|
+
let side: RectSide = "left"
|
|
39
|
+
|
|
40
|
+
if (dr < closest) {
|
|
41
|
+
closest = dr
|
|
42
|
+
side = "right"
|
|
43
|
+
}
|
|
44
|
+
if (dt < closest) {
|
|
45
|
+
closest = dt
|
|
46
|
+
side = "top"
|
|
47
|
+
}
|
|
48
|
+
if (db < closest) {
|
|
49
|
+
side = "bottom"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return side
|
|
53
|
+
}
|
package/src/contains.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { getRectCorners, isRect, Rect } from "./rect"
|
|
2
|
+
import type { Point } from "./types"
|
|
3
|
+
|
|
4
|
+
export function containsPoint(r: Rect, p: Point): boolean {
|
|
5
|
+
return r.minX <= p.x && p.x <= r.maxX && r.minY <= p.y && p.y <= r.maxY
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function containsRect(a: Rect, b: Rect): boolean {
|
|
9
|
+
return Object.values(getRectCorners(b)).every((c) => containsPoint(a, c))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function contains(r: Rect, v: Rect | Point): boolean {
|
|
13
|
+
return isRect(v) ? containsRect(r, v) : containsPoint(r, v)
|
|
14
|
+
}
|
package/src/distance.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { intersects } from "./intersection"
|
|
2
|
+
import type { Rect } from "./rect"
|
|
3
|
+
import type { Point, RectSide } from "./types"
|
|
4
|
+
|
|
5
|
+
export type DistanceValue = Point & {
|
|
6
|
+
value: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function distance(a: Point, b: Point = { x: 0, y: 0 }): number {
|
|
10
|
+
return Math.sqrt(Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function distanceFromPoint(r: Rect, p: Point): DistanceValue {
|
|
14
|
+
let x = 0
|
|
15
|
+
let y = 0
|
|
16
|
+
if (p.x < r.x) x = r.x - p.x
|
|
17
|
+
else if (p.x > r.maxX) x = p.x - r.maxX
|
|
18
|
+
if (p.y < r.y) y = r.y - p.y
|
|
19
|
+
else if (p.y > r.maxY) y = p.y - r.maxY
|
|
20
|
+
return { x, y, value: distance({ x, y }) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function distanceFromRect(a: Rect, b: Rect): DistanceValue {
|
|
24
|
+
if (intersects(a, b)) return { x: 0, y: 0, value: 0 }
|
|
25
|
+
const left = a.x < b.x ? a : b
|
|
26
|
+
const right = b.x < a.x ? a : b
|
|
27
|
+
const upper = a.y < b.y ? a : b
|
|
28
|
+
const lower = b.y < a.y ? a : b
|
|
29
|
+
let x = left.x === right.x ? 0 : right.x - left.maxX
|
|
30
|
+
x = Math.max(0, x)
|
|
31
|
+
let y = upper.y === lower.y ? 0 : lower.y - upper.maxY
|
|
32
|
+
y = Math.max(0, y)
|
|
33
|
+
return { x, y, value: distance({ x, y }) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function distanceBtwEdges(a: Rect, b: Rect): Record<RectSide, number> {
|
|
37
|
+
return {
|
|
38
|
+
left: b.x - a.x,
|
|
39
|
+
top: b.y - a.y,
|
|
40
|
+
right: a.maxX - b.maxX,
|
|
41
|
+
bottom: a.maxY - b.maxY,
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createRect, Rect } from "./rect"
|
|
2
|
+
|
|
3
|
+
const styleCache = new WeakMap<HTMLElement, any>()
|
|
4
|
+
function getCacheComputedStyle(el: HTMLElement) {
|
|
5
|
+
if (!styleCache.has(el)) {
|
|
6
|
+
const win = el.ownerDocument.defaultView || window
|
|
7
|
+
styleCache.set(el, win.getComputedStyle(el))
|
|
8
|
+
}
|
|
9
|
+
return styleCache.get(el)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getElementRect(el: HTMLElement, opts: ElementRectOptions = {}): Rect {
|
|
13
|
+
return createRect(getClientRect(el, opts))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type ElementRectOptions = {
|
|
17
|
+
/**
|
|
18
|
+
* Whether to exclude the element's scrollbar size from the calculation.
|
|
19
|
+
*/
|
|
20
|
+
excludeScrollbar?: boolean
|
|
21
|
+
/**
|
|
22
|
+
* Whether to exclude the element's borders from the calculation.
|
|
23
|
+
*/
|
|
24
|
+
excludeBorders?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getClientRect(el: HTMLElement, opts: ElementRectOptions = {}) {
|
|
28
|
+
const { excludeScrollbar = false, excludeBorders = false } = opts
|
|
29
|
+
|
|
30
|
+
const { x, y, width, height } = el.getBoundingClientRect()
|
|
31
|
+
const r = { x, y, width, height }
|
|
32
|
+
|
|
33
|
+
const style = getCacheComputedStyle(el)
|
|
34
|
+
|
|
35
|
+
const { borderLeftWidth, borderTopWidth, borderRightWidth, borderBottomWidth } = style
|
|
36
|
+
|
|
37
|
+
const borderXWidth = sum(borderLeftWidth, borderRightWidth)
|
|
38
|
+
const borderYWidth = sum(borderTopWidth, borderBottomWidth)
|
|
39
|
+
|
|
40
|
+
if (excludeBorders) {
|
|
41
|
+
r.width -= borderXWidth
|
|
42
|
+
r.height -= borderYWidth
|
|
43
|
+
r.x += px(borderLeftWidth)
|
|
44
|
+
r.y += px(borderTopWidth)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (excludeScrollbar) {
|
|
48
|
+
const scrollbarWidth = el.offsetWidth - el.clientWidth - borderXWidth
|
|
49
|
+
const scrollbarHeight = el.offsetHeight - el.clientHeight - borderYWidth
|
|
50
|
+
r.width -= scrollbarWidth
|
|
51
|
+
r.height -= scrollbarHeight
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return r
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const px = (v: string) => parseFloat(v.replace("px", ""))
|
|
58
|
+
|
|
59
|
+
const sum = (...vals: string[]) => vals.reduce((sum, v) => sum + (v ? px(v) : 0), 0)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRect, Rect } from "./rect"
|
|
2
|
+
import type { Point } from "./types"
|
|
3
|
+
|
|
4
|
+
export function getRectFromPoints(...pts: Point[]): Rect {
|
|
5
|
+
const xs = pts.map((p) => p.x)
|
|
6
|
+
const ys = pts.map((p) => p.y)
|
|
7
|
+
|
|
8
|
+
const x = Math.min(...xs)
|
|
9
|
+
const y = Math.min(...ys)
|
|
10
|
+
|
|
11
|
+
const width = Math.max(...xs) - x
|
|
12
|
+
const height = Math.max(...ys) - y
|
|
13
|
+
|
|
14
|
+
return createRect({ x, y, width, height })
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createRect, Rect } from "./rect"
|
|
2
|
+
import { getElementRect } from "./from-element"
|
|
3
|
+
import { union } from "./union"
|
|
4
|
+
|
|
5
|
+
export function fromRange(range: Range): Rect {
|
|
6
|
+
let rs: Rect[] = []
|
|
7
|
+
const rects = Array.from(range.getClientRects())
|
|
8
|
+
|
|
9
|
+
if (rects.length) {
|
|
10
|
+
rs = rs.concat(rects.map(createRect))
|
|
11
|
+
return union.apply(undefined, rs)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let start: Node | ParentNode | null = range.startContainer
|
|
15
|
+
|
|
16
|
+
if (start.nodeType === Node.TEXT_NODE) {
|
|
17
|
+
start = start.parentNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (start instanceof HTMLElement) {
|
|
21
|
+
const r = getElementRect(start)
|
|
22
|
+
rs.push({ ...r, x: r.maxX, width: 0 })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return union.apply(undefined, rs)
|
|
26
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createRect, getRectCorners, Rect } from "./rect"
|
|
2
|
+
import type { Point } from "./types"
|
|
3
|
+
|
|
4
|
+
export function toRad(d: number) {
|
|
5
|
+
return ((d % 360) * Math.PI) / 180
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function rotate(a: Point, d: number, c: Point): Point {
|
|
9
|
+
const r = toRad(d)
|
|
10
|
+
|
|
11
|
+
const sin = Math.sin(r)
|
|
12
|
+
const cos = Math.cos(r)
|
|
13
|
+
|
|
14
|
+
const x = a.x - c.x
|
|
15
|
+
const y = a.y - c.y
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
x: c.x + x * cos - y * sin,
|
|
19
|
+
y: c.y + x * sin + y * cos,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getRotationRect(r: Rect, deg: number): Rect {
|
|
24
|
+
const rr = Object.values(getRectCorners(r)).map((p) => rotate(p, deg, r.center))
|
|
25
|
+
|
|
26
|
+
const xs = rr.map((p) => p.x)
|
|
27
|
+
const ys = rr.map((p) => p.y)
|
|
28
|
+
|
|
29
|
+
const minX = Math.min(...xs)
|
|
30
|
+
const minY = Math.min(...ys)
|
|
31
|
+
|
|
32
|
+
const maxX = Math.max(...xs)
|
|
33
|
+
const maxY = Math.max(...ys)
|
|
34
|
+
|
|
35
|
+
return createRect({
|
|
36
|
+
x: minX,
|
|
37
|
+
y: minY,
|
|
38
|
+
width: maxX - minX,
|
|
39
|
+
height: maxY - minY,
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createRect, Rect } from "./rect"
|
|
2
|
+
|
|
3
|
+
export type WindowRectOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* Whether to exclude the element's scrollbar size from the calculation.
|
|
6
|
+
*/
|
|
7
|
+
excludeScrollbar?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Creates a rectange from window object
|
|
12
|
+
*/
|
|
13
|
+
export function getWindowRect(win: Window, opts: WindowRectOptions = {}): Rect {
|
|
14
|
+
return createRect(getViewportRect(win, opts))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the rect of the window with the option to exclude the scrollbar
|
|
19
|
+
*/
|
|
20
|
+
export function getViewportRect(win: Window, opts: WindowRectOptions) {
|
|
21
|
+
const { excludeScrollbar = false } = opts
|
|
22
|
+
const { innerWidth, innerHeight, document: doc, visualViewport } = win
|
|
23
|
+
const width = visualViewport?.width || innerWidth
|
|
24
|
+
const height = visualViewport?.height || innerHeight
|
|
25
|
+
const rect = { x: 0, y: 0, width, height }
|
|
26
|
+
if (excludeScrollbar) {
|
|
27
|
+
const scrollbarWidth = innerWidth - doc.documentElement.clientWidth
|
|
28
|
+
const scrollbarHeight = innerHeight - doc.documentElement.clientHeight
|
|
29
|
+
rect.width -= scrollbarWidth
|
|
30
|
+
rect.height -= scrollbarHeight
|
|
31
|
+
}
|
|
32
|
+
return rect
|
|
33
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createRect, getRectCorners } from "./rect"
|
|
2
|
+
import type { RectValue } from "./types"
|
|
3
|
+
|
|
4
|
+
export function getElementPolygon(rectValue: RectValue, placement: string) {
|
|
5
|
+
const rect = createRect(rectValue)
|
|
6
|
+
const { top, right, left, bottom } = getRectCorners(rect)
|
|
7
|
+
const [base] = placement.split("-")
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
top: [left, top, right, bottom],
|
|
11
|
+
right: [top, right, bottom, left],
|
|
12
|
+
bottom: [top, left, bottom, right],
|
|
13
|
+
left: [right, top, left, bottom],
|
|
14
|
+
}[base]
|
|
15
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export * from "./align"
|
|
2
|
+
export * from "./closest"
|
|
3
|
+
export * from "./contains"
|
|
4
|
+
export * from "./distance"
|
|
5
|
+
export * from "./from-element"
|
|
6
|
+
export * from "./from-points"
|
|
7
|
+
export * from "./from-range"
|
|
8
|
+
export * from "./from-rotation"
|
|
9
|
+
export * from "./from-window"
|
|
10
|
+
export * from "./get-polygon"
|
|
11
|
+
export * from "./intersection"
|
|
12
|
+
export * from "./operations"
|
|
13
|
+
export * from "./polygon"
|
|
14
|
+
export * from "./rect"
|
|
15
|
+
export * from "./types"
|
|
16
|
+
export * from "./union"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { createRect, Rect } from "./rect"
|
|
2
|
+
import type { RectSide } from "./types"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Checks if a Rect intersects another Rect
|
|
6
|
+
*/
|
|
7
|
+
export function intersects(a: Rect, b: Rect): boolean {
|
|
8
|
+
return a.x < b.maxX && a.y < b.maxY && a.maxX > b.x && a.maxY > b.y
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Returns a new Rect that represents the intersection between two Rects
|
|
13
|
+
*/
|
|
14
|
+
export function intersection(a: Rect, b: Rect): Rect {
|
|
15
|
+
const x = Math.max(a.x, b.x)
|
|
16
|
+
const y = Math.max(a.y, b.y)
|
|
17
|
+
const x2 = Math.min(a.x + a.width, b.x + b.width)
|
|
18
|
+
const y2 = Math.min(a.y + a.height, b.y + b.height)
|
|
19
|
+
return createRect({ x, y, width: x2 - x, height: y2 - y })
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Returns whether two rects collide along each edge
|
|
24
|
+
*/
|
|
25
|
+
export function collisions(a: Rect, b: Rect): Record<RectSide, boolean> {
|
|
26
|
+
return {
|
|
27
|
+
top: a.minY <= b.minY,
|
|
28
|
+
right: a.maxX >= b.maxX,
|
|
29
|
+
bottom: a.maxY >= b.maxY,
|
|
30
|
+
left: a.minX <= b.minX,
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createRect, Rect } from "./rect"
|
|
2
|
+
import type { Point, RectInset, SymmetricRectInset } from "./types"
|
|
3
|
+
|
|
4
|
+
export const isSymmetric = (v: any): v is SymmetricRectInset => "dx" in v || "dy" in v
|
|
5
|
+
|
|
6
|
+
export function inset(r: Rect, i: RectInset | SymmetricRectInset): Rect {
|
|
7
|
+
const v = isSymmetric(i) ? { left: i.dx, right: i.dx, top: i.dy, bottom: i.dy } : i
|
|
8
|
+
const { top = 0, right = 0, bottom = 0, left = 0 } = v
|
|
9
|
+
return createRect({
|
|
10
|
+
x: r.x + left,
|
|
11
|
+
y: r.y + top,
|
|
12
|
+
width: r.width - left - right,
|
|
13
|
+
height: r.height - top - bottom,
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function expand(r: Rect, v: number | SymmetricRectInset): Rect {
|
|
18
|
+
const value = typeof v === "number" ? { dx: -v, dy: -v } : v
|
|
19
|
+
return inset(r, value)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function shrink(r: Rect, v: number | SymmetricRectInset): Rect {
|
|
23
|
+
const value = typeof v === "number" ? { dx: -v, dy: -v } : v
|
|
24
|
+
return inset(r, value)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function shift(r: Rect, o: Partial<Point>): Rect {
|
|
28
|
+
const { x = 0, y = 0 } = o
|
|
29
|
+
return createRect({
|
|
30
|
+
x: r.x + x,
|
|
31
|
+
y: r.y + y,
|
|
32
|
+
width: r.width,
|
|
33
|
+
height: r.height,
|
|
34
|
+
})
|
|
35
|
+
}
|
package/src/polygon.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Point } from "./types"
|
|
2
|
+
|
|
3
|
+
export function isPointInPolygon(polygon: Point[], point: Point) {
|
|
4
|
+
const { x, y } = point
|
|
5
|
+
let c = false
|
|
6
|
+
|
|
7
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
8
|
+
const xi = polygon[i].x
|
|
9
|
+
const yi = polygon[i].y
|
|
10
|
+
const xj = polygon[j].x
|
|
11
|
+
const yj = polygon[j].y
|
|
12
|
+
|
|
13
|
+
if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) {
|
|
14
|
+
c = !c
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return c
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function createPolygonElement() {
|
|
21
|
+
const id = "debug-polygon"
|
|
22
|
+
const existingPolygon = document.getElementById(id)
|
|
23
|
+
if (existingPolygon) {
|
|
24
|
+
return existingPolygon
|
|
25
|
+
}
|
|
26
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg")
|
|
27
|
+
Object.assign(svg.style, {
|
|
28
|
+
top: "0",
|
|
29
|
+
left: "0",
|
|
30
|
+
width: "100%",
|
|
31
|
+
height: "100%",
|
|
32
|
+
opacity: "0.15",
|
|
33
|
+
position: "fixed",
|
|
34
|
+
pointerEvents: "none",
|
|
35
|
+
fill: "red",
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon")
|
|
39
|
+
polygon.setAttribute("id", id)
|
|
40
|
+
polygon.setAttribute("points", "0,0 0,0")
|
|
41
|
+
svg.appendChild(polygon)
|
|
42
|
+
document.body.appendChild(svg)
|
|
43
|
+
return polygon
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function debugPolygon(polygon: Point[]) {
|
|
47
|
+
const el = createPolygonElement()
|
|
48
|
+
const points = polygon.map((point) => `${point.x},${point.y}`).join(" ")
|
|
49
|
+
el.setAttribute("points", points)
|
|
50
|
+
return () => {
|
|
51
|
+
el.remove()
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/rect.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { RectEdge, RectValue } from "./types"
|
|
2
|
+
|
|
3
|
+
const point = (x: number, y: number) => ({ x, y })
|
|
4
|
+
|
|
5
|
+
export function createRect(r: RectValue) {
|
|
6
|
+
const { x, y, width, height } = r
|
|
7
|
+
const midX = x + width / 2
|
|
8
|
+
const midY = y + height / 2
|
|
9
|
+
return {
|
|
10
|
+
x,
|
|
11
|
+
y,
|
|
12
|
+
width,
|
|
13
|
+
height,
|
|
14
|
+
minX: x,
|
|
15
|
+
minY: y,
|
|
16
|
+
maxX: x + width,
|
|
17
|
+
maxY: y + height,
|
|
18
|
+
midX,
|
|
19
|
+
midY,
|
|
20
|
+
center: point(midX, midY),
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type Rect = ReturnType<typeof createRect>
|
|
25
|
+
|
|
26
|
+
const hasProp = <T extends string>(obj: any, prop: T): obj is Record<T, any> =>
|
|
27
|
+
Object.prototype.hasOwnProperty.call(obj, prop)
|
|
28
|
+
|
|
29
|
+
export function isRect(v: any): v is Rect {
|
|
30
|
+
return hasProp(v, "x") && hasProp(v, "y") && hasProp(v, "width") && hasProp(v, "height")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function getRectCenters(v: Rect) {
|
|
34
|
+
const top = point(v.midX, v.minY)
|
|
35
|
+
const right = point(v.maxX, v.midY)
|
|
36
|
+
const bottom = point(v.midX, v.maxY)
|
|
37
|
+
const left = point(v.minX, v.midY)
|
|
38
|
+
return { top, right, bottom, left }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getRectCorners(v: Rect) {
|
|
42
|
+
const top = point(v.minX, v.minY)
|
|
43
|
+
const right = point(v.maxX, v.minY)
|
|
44
|
+
const bottom = point(v.maxX, v.maxY)
|
|
45
|
+
const left = point(v.minX, v.maxY)
|
|
46
|
+
return { top, right, bottom, left }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getRectEdges(v: Rect) {
|
|
50
|
+
const c = getRectCorners(v)
|
|
51
|
+
const top: RectEdge = [c.top, c.right]
|
|
52
|
+
const right: RectEdge = [c.right, c.bottom]
|
|
53
|
+
const bottom: RectEdge = [c.left, c.bottom]
|
|
54
|
+
const left: RectEdge = [c.top, c.left]
|
|
55
|
+
return { top, right, bottom, left }
|
|
56
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type Point = { x: number; y: number }
|
|
2
|
+
|
|
3
|
+
export type RectValue = {
|
|
4
|
+
x: number
|
|
5
|
+
y: number
|
|
6
|
+
width: number
|
|
7
|
+
height: number
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type RectSide = "top" | "right" | "bottom" | "left"
|
|
11
|
+
|
|
12
|
+
export type RectPoint =
|
|
13
|
+
| "top-left"
|
|
14
|
+
| "top-center"
|
|
15
|
+
| "top-right"
|
|
16
|
+
| "right-center"
|
|
17
|
+
| "left-center"
|
|
18
|
+
| "bottom-left"
|
|
19
|
+
| "bottom-right"
|
|
20
|
+
| "bottom-center"
|
|
21
|
+
| "center"
|
|
22
|
+
|
|
23
|
+
export type RectEdge = [Point, Point]
|
|
24
|
+
|
|
25
|
+
export type RectPoints = [Point, Point, Point, Point]
|
|
26
|
+
|
|
27
|
+
export type RectEdges = Record<RectSide, RectEdge> & {
|
|
28
|
+
value: RectEdge[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type RectCorner = "topLeft" | "topRight" | "bottomLeft" | "bottomRight"
|
|
32
|
+
|
|
33
|
+
export type RectCorners = Record<RectCorner, Point> & {
|
|
34
|
+
value: RectPoints
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type RectCenter = "topCenter" | "rightCenter" | "leftCenter" | "bottomCenter"
|
|
38
|
+
|
|
39
|
+
export type RectCenters = Record<RectCenter, Point> & {
|
|
40
|
+
value: RectPoints
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type RectInset = Partial<Record<RectSide, number>>
|
|
44
|
+
|
|
45
|
+
export type SymmetricRectInset = { dx?: number; dy?: number }
|
package/src/union.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { getRectFromPoints } from "./from-points"
|
|
2
|
+
import type { Rect } from "./rect"
|
|
3
|
+
|
|
4
|
+
const { min, max } = Math
|
|
5
|
+
|
|
6
|
+
export function union(...rs: Rect[]): Rect {
|
|
7
|
+
const pMin = {
|
|
8
|
+
x: min.apply(
|
|
9
|
+
Math,
|
|
10
|
+
rs.map((r) => r.minX),
|
|
11
|
+
),
|
|
12
|
+
y: min.apply(
|
|
13
|
+
Math,
|
|
14
|
+
rs.map((r) => r.minY),
|
|
15
|
+
),
|
|
16
|
+
}
|
|
17
|
+
const pMax = {
|
|
18
|
+
x: max.apply(
|
|
19
|
+
Math,
|
|
20
|
+
rs.map((r) => r.maxX),
|
|
21
|
+
),
|
|
22
|
+
y: max.apply(
|
|
23
|
+
Math,
|
|
24
|
+
rs.map((r) => r.maxY),
|
|
25
|
+
),
|
|
26
|
+
}
|
|
27
|
+
return getRectFromPoints(pMin, pMax)
|
|
28
|
+
}
|