@zag-js/rect-utils 0.9.2 → 0.10.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zag-js/rect-utils",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
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
+ }
@@ -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
+ }
@@ -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
+ }