Bläddra i källkod

Adds bounds

main
Steve Ruiz 4 år sedan
förälder
incheckning
89d9ddcb1d

+ 33
- 1
components/canvas/bounds-bg.tsx Visa fil

@@ -1 +1,33 @@
1
-export default function BoundsBg() {}
1
+import state, { useSelector } from "state"
2
+import styled from "styles"
3
+
4
+export default function BoundsBg() {
5
+  const bounds = useSelector((state) => state.values.selectedBounds)
6
+
7
+  if (!bounds) return null
8
+
9
+  const { minX, minY, width, height } = bounds
10
+
11
+  return (
12
+    <StyledBoundsBg
13
+      x={minX}
14
+      y={minY}
15
+      width={width}
16
+      height={height}
17
+      onPointerDown={(e) => {
18
+        if (e.buttons !== 1) return
19
+        state.send("POINTED_BOUNDS", {
20
+          shiftKey: e.shiftKey,
21
+          optionKey: e.altKey,
22
+          metaKey: e.metaKey || e.ctrlKey,
23
+          ctrlKey: e.ctrlKey,
24
+          buttons: e.buttons,
25
+        })
26
+      }}
27
+    />
28
+  )
29
+}
30
+
31
+const StyledBoundsBg = styled("rect", {
32
+  fill: "$boundsBg",
33
+})

+ 297
- 1
components/canvas/bounds.tsx Visa fil

@@ -1 +1,297 @@
1
-export default function Bounds() {}
1
+import state, { useSelector } from "state"
2
+import { motion } from "framer-motion"
3
+import styled from "styles"
4
+
5
+export default function Bounds() {
6
+  const bounds = useSelector((state) => state.values.selectedBounds)
7
+  const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
8
+  const zoom = useSelector((state) => state.data.camera.zoom)
9
+
10
+  if (!bounds) return null
11
+
12
+  const { minX, minY, maxX, maxY, width, height } = bounds
13
+
14
+  const p = 4 / zoom
15
+  const cp = p * 2
16
+
17
+  return (
18
+    <g pointerEvents={isBrushing ? "none" : "all"}>
19
+      <StyledBounds
20
+        x={minX}
21
+        y={minY}
22
+        width={width}
23
+        height={height}
24
+        pointerEvents="none"
25
+      />
26
+      <Corner
27
+        x={minX}
28
+        y={minY}
29
+        corner={0}
30
+        width={cp}
31
+        height={cp}
32
+        cursor="nwse-resize"
33
+      />
34
+      <Corner
35
+        x={maxX}
36
+        y={minY}
37
+        corner={1}
38
+        width={cp}
39
+        height={cp}
40
+        cursor="nesw-resize"
41
+      />
42
+      <Corner
43
+        x={maxX}
44
+        y={maxY}
45
+        corner={2}
46
+        width={cp}
47
+        height={cp}
48
+        cursor="nwse-resize"
49
+      />
50
+      <Corner
51
+        x={minX}
52
+        y={maxY}
53
+        corner={3}
54
+        width={cp}
55
+        height={cp}
56
+        cursor="nesw-resize"
57
+      />
58
+      <EdgeHorizontal
59
+        x={minX + p}
60
+        y={minY}
61
+        width={Math.max(0, width - p * 2)}
62
+        height={p}
63
+        onSelect={(e) => {
64
+          e.stopPropagation()
65
+          if (e.buttons !== 1) return
66
+          state.send("POINTED_BOUNDS_EDGE", {
67
+            edge: 0,
68
+            shiftKey: e.shiftKey,
69
+            optionKey: e.altKey,
70
+            metaKey: e.metaKey,
71
+            ctrlKey: e.ctrlKey,
72
+            buttons: e.buttons,
73
+          })
74
+          document.body.style.cursor = "ns-resize"
75
+        }}
76
+      />
77
+      <EdgeVertical
78
+        x={maxX}
79
+        y={minY + p}
80
+        width={p}
81
+        height={Math.max(0, height - p * 2)}
82
+        onSelect={(e) => {
83
+          e.stopPropagation()
84
+          if (e.buttons !== 1) return
85
+          state.send("POINTED_BOUNDS_EDGE", {
86
+            edge: 1,
87
+            shiftKey: e.shiftKey,
88
+            optionKey: e.altKey,
89
+            metaKey: e.metaKey,
90
+            ctrlKey: e.ctrlKey,
91
+            buttons: e.buttons,
92
+          })
93
+          document.body.style.cursor = "ew-resize"
94
+        }}
95
+      />
96
+      <EdgeHorizontal
97
+        x={minX + p}
98
+        y={maxY}
99
+        width={Math.max(0, width - p * 2)}
100
+        height={p}
101
+        onSelect={(e) => {
102
+          e.stopPropagation()
103
+          if (e.buttons !== 1) return
104
+          state.send("POINTED_BOUNDS_EDGE", {
105
+            edge: 2,
106
+            shiftKey: e.shiftKey,
107
+            optionKey: e.altKey,
108
+            metaKey: e.metaKey,
109
+            ctrlKey: e.ctrlKey,
110
+            buttons: e.buttons,
111
+          })
112
+          document.body.style.cursor = "ns-resize"
113
+        }}
114
+      />
115
+      <EdgeVertical
116
+        x={minX}
117
+        y={minY + p}
118
+        width={p}
119
+        height={Math.max(0, height - p * 2)}
120
+        onSelect={(e) => {
121
+          e.stopPropagation()
122
+          if (e.buttons !== 1) return
123
+          state.send("POINTED_BOUNDS_EDGE", {
124
+            edge: 3,
125
+            shiftKey: e.shiftKey,
126
+            optionKey: e.altKey,
127
+            metaKey: e.metaKey,
128
+            ctrlKey: e.ctrlKey,
129
+            buttons: e.buttons,
130
+          })
131
+          document.body.style.cursor = "ew-resize"
132
+        }}
133
+      />
134
+    </g>
135
+  )
136
+}
137
+
138
+function Corner({
139
+  x,
140
+  y,
141
+  width,
142
+  height,
143
+  cursor,
144
+  onHover,
145
+  corner,
146
+}: {
147
+  x: number
148
+  y: number
149
+  width: number
150
+  height: number
151
+  cursor: string
152
+  corner: number
153
+  onHover?: () => void
154
+}) {
155
+  const isTop = corner === 0 || corner === 1
156
+  const isLeft = corner === 0 || corner === 3
157
+  return (
158
+    <g>
159
+      <motion.rect
160
+        x={x + width * (isLeft ? -1.25 : -0.5)} // + width * 2 * transformOffset[0]}
161
+        y={y + width * (isTop ? -1.25 : -0.5)} // + height * 2 * transformOffset[1]}
162
+        width={width * 1.75}
163
+        height={height * 1.75}
164
+        onPanEnd={restoreCursor}
165
+        onTap={restoreCursor}
166
+        onPointerDown={(e) => {
167
+          e.stopPropagation()
168
+          if (e.buttons !== 1) return
169
+          state.send("POINTED_ROTATE_CORNER", {
170
+            corner,
171
+            shiftKey: e.shiftKey,
172
+            optionKey: e.altKey,
173
+            metaKey: e.metaKey,
174
+            ctrlKey: e.ctrlKey,
175
+            buttons: e.buttons,
176
+          })
177
+          document.body.style.cursor = "grabbing"
178
+        }}
179
+        style={{ cursor: "grab" }}
180
+        fill="transparent"
181
+      />
182
+      <StyledCorner
183
+        x={x + width * -0.5}
184
+        y={y + height * -0.5}
185
+        width={width}
186
+        height={height}
187
+        onPointerEnter={onHover}
188
+        onPointerDown={(e) => {
189
+          e.stopPropagation()
190
+          if (e.buttons !== 1) return
191
+          state.send("POINTED_BOUNDS_CORNER", {
192
+            corner,
193
+            shiftKey: e.shiftKey,
194
+            optionKey: e.altKey,
195
+            metaKey: e.metaKey,
196
+            ctrlKey: e.ctrlKey,
197
+            buttons: e.buttons,
198
+          })
199
+          document.body.style.cursor = "nesw-resize"
200
+        }}
201
+        onPanEnd={restoreCursor}
202
+        onTap={restoreCursor}
203
+        style={{ cursor }}
204
+        className="strokewidth-ui stroke-bounds fill-corner"
205
+      />
206
+    </g>
207
+  )
208
+}
209
+
210
+function EdgeHorizontal({
211
+  x,
212
+  y,
213
+  width,
214
+  height,
215
+  onHover,
216
+  onSelect,
217
+}: {
218
+  x: number
219
+  y: number
220
+  width: number
221
+  height: number
222
+  onHover?: () => void
223
+  onSelect?: (e: React.PointerEvent) => void
224
+}) {
225
+  return (
226
+    <StyledEdge
227
+      x={x}
228
+      y={y - height / 2}
229
+      width={width}
230
+      height={height}
231
+      onPointerEnter={onHover}
232
+      onPointerDown={onSelect}
233
+      onPanEnd={restoreCursor}
234
+      onTap={restoreCursor}
235
+      style={{ cursor: "ns-resize" }}
236
+      direction="horizontal"
237
+    />
238
+  )
239
+}
240
+
241
+function EdgeVertical({
242
+  x,
243
+  y,
244
+  width,
245
+  height,
246
+  onHover,
247
+  onSelect,
248
+}: {
249
+  x: number
250
+  y: number
251
+  width: number
252
+  height: number
253
+  onHover?: () => void
254
+  onSelect?: (e: React.PointerEvent) => void
255
+}) {
256
+  return (
257
+    <StyledEdge
258
+      x={x - width / 2}
259
+      y={y}
260
+      width={width}
261
+      height={height}
262
+      onPointerEnter={onHover}
263
+      onPointerDown={onSelect}
264
+      onPanEnd={restoreCursor}
265
+      onTap={restoreCursor}
266
+      direction="vertical"
267
+    />
268
+  )
269
+}
270
+
271
+function restoreCursor() {
272
+  document.body.style.cursor = "default"
273
+  state.send("STOPPED_POINTING")
274
+}
275
+
276
+const StyledEdge = styled(motion.rect, {
277
+  stroke: "none",
278
+  fill: "none",
279
+  variant: {
280
+    direction: {
281
+      horizontal: { cursor: "ns-resize" },
282
+      vertical: { cursor: "ew-resize" },
283
+    },
284
+  },
285
+})
286
+
287
+const StyledCorner = styled(motion.rect, {
288
+  stroke: "$bounds",
289
+  fill: "#fff",
290
+  zStrokeWidth: 2,
291
+})
292
+
293
+const StyledBounds = styled("rect", {
294
+  fill: "none",
295
+  stroke: "$bounds",
296
+  zStrokeWidth: 2,
297
+})

+ 4
- 0
components/canvas/canvas.tsx Visa fil

@@ -6,6 +6,8 @@ import useCamera from "hooks/useCamera"
6 6
 import Page from "./page"
7 7
 import Brush from "./brush"
8 8
 import state from "state"
9
+import Bounds from "./bounds"
10
+import BoundsBg from "./bounds-bg"
9 11
 
10 12
 export default function Canvas() {
11 13
   const rCanvas = useRef<SVGSVGElement>(null)
@@ -37,7 +39,9 @@ export default function Canvas() {
37 39
       onPointerUp={handlePointerUp}
38 40
     >
39 41
       <MainGroup ref={rGroup}>
42
+        <BoundsBg />
40 43
         <Page />
44
+        <Bounds />
41 45
         <Brush />
42 46
       </MainGroup>
43 47
     </MainSVG>

+ 6
- 17
components/canvas/shape.tsx Visa fil

@@ -1,19 +1,8 @@
1
-import React, { useCallback, useRef } from "react"
1
+import React, { useCallback, useRef, memo } from "react"
2 2
 import state, { useSelector } from "state"
3
-import styled from "styles"
4 3
 import { getPointerEventInfo } from "utils/utils"
5
-import { memo } from "react"
6
-import Shapes from "lib/shapes"
7
-
8
-/*
9
-Gets the shape from the current page's shapes, using the
10
-provided ID. Depending on the shape's type, return the
11
-component for that type.
12
-
13
-This component takes an SVG shape as its children. It handles
14
-events for the shape as well as provides indicators for hover
15
- and selected status
16
-*/
4
+import shapes from "lib/shapes"
5
+import styled from "styles"
17 6
 
18 7
 function Shape({ id }: { id: string }) {
19 8
   const rGroup = useRef<SVGGElement>(null)
@@ -66,7 +55,7 @@ function Shape({ id }: { id: string }) {
66 55
       onPointerLeave={handlePointerLeave}
67 56
     >
68 57
       <defs>
69
-        {Shapes[shape.type] ? Shapes[shape.type].render(shape) : null}
58
+        {shapes[shape.type] ? shapes[shape.type].render(shape) : null}
70 59
       </defs>
71 60
       <HoverIndicator as="use" xlinkHref={"#" + id} />
72 61
       <use xlinkHref={"#" + id} {...shape.style} />
@@ -78,7 +67,7 @@ function Shape({ id }: { id: string }) {
78 67
 const Indicator = styled("path", {
79 68
   fill: "none",
80 69
   stroke: "transparent",
81
-  strokeWidth: "max(1, calc(2 / var(--camera-zoom)))",
70
+  zStrokeWidth: 1,
82 71
   pointerEvents: "none",
83 72
   strokeLineCap: "round",
84 73
   strokeLinejoin: "round",
@@ -87,7 +76,7 @@ const Indicator = styled("path", {
87 76
 const HoverIndicator = styled("path", {
88 77
   fill: "none",
89 78
   stroke: "transparent",
90
-  strokeWidth: "max(1, calc(8 / var(--camera-zoom)))",
79
+  zStrokeWidth: 8,
91 80
   pointerEvents: "all",
92 81
   strokeLinecap: "round",
93 82
   strokeLinejoin: "round",

+ 14
- 6
lib/shapes/circle.tsx Visa fil

@@ -1,6 +1,7 @@
1 1
 import { v4 as uuid } from "uuid"
2 2
 import * as vec from "utils/vec"
3 3
 import { BaseLibShape, CircleShape, ShapeType } from "types"
4
+import { boundsCache } from "./index"
4 5
 
5 6
 const Circle: BaseLibShape<ShapeType.Circle> = {
6 7
   create(props): CircleShape {
@@ -23,19 +24,26 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
23 24
   },
24 25
 
25 26
   getBounds(shape) {
27
+    if (boundsCache.has(shape)) {
28
+      return boundsCache.get(shape)
29
+    }
30
+
26 31
     const {
27
-      point: [cx, cy],
32
+      point: [x, y],
28 33
       radius,
29 34
     } = shape
30 35
 
31
-    return {
32
-      minX: cx,
33
-      maxX: cx + radius * 2,
34
-      minY: cy,
35
-      maxY: cy + radius * 2,
36
+    const bounds = {
37
+      minX: x,
38
+      maxX: x + radius * 2,
39
+      minY: y,
40
+      maxY: y + radius * 2,
36 41
       width: radius * 2,
37 42
       height: radius * 2,
38 43
     }
44
+
45
+    boundsCache.set(shape, bounds)
46
+    return bounds
39 47
   },
40 48
 
41 49
   hitTest(shape, test) {

+ 16
- 8
lib/shapes/dot.tsx Visa fil

@@ -1,6 +1,7 @@
1 1
 import { v4 as uuid } from "uuid"
2 2
 import * as vec from "utils/vec"
3 3
 import { BaseLibShape, DotShape, ShapeType } from "types"
4
+import { boundsCache } from "./index"
4 5
 
5 6
 const Dot: BaseLibShape<ShapeType.Dot> = {
6 7
   create(props): DotShape {
@@ -22,18 +23,25 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
22 23
   },
23 24
 
24 25
   getBounds(shape) {
26
+    if (boundsCache.has(shape)) {
27
+      return boundsCache.get(shape)
28
+    }
29
+
25 30
     const {
26
-      point: [cx, cy],
31
+      point: [x, y],
27 32
     } = shape
28 33
 
29
-    return {
30
-      minX: cx,
31
-      maxX: cx + 4,
32
-      minY: cy,
33
-      maxY: cy + 4,
34
-      width: 4,
35
-      height: 4,
34
+    const bounds = {
35
+      minX: x,
36
+      maxX: x + 8,
37
+      minY: y,
38
+      maxY: y + 8,
39
+      width: 8,
40
+      height: 8,
36 41
     }
42
+
43
+    boundsCache.set(shape, bounds)
44
+    return bounds
37 45
   },
38 46
 
39 47
   hitTest(shape, test) {

+ 6
- 2
lib/shapes/index.tsx Visa fil

@@ -3,11 +3,15 @@ import Dot from "./dot"
3 3
 import Polyline from "./polyline"
4 4
 import Rectangle from "./rectangle"
5 5
 
6
-import { ShapeType } from "types"
6
+import { Bounds, Shape, ShapeType } from "types"
7 7
 
8
-export default {
8
+export const boundsCache = new WeakMap<Shape, Bounds>([])
9
+
10
+const shapes = {
9 11
   [ShapeType.Circle]: Circle,
10 12
   [ShapeType.Dot]: Dot,
11 13
   [ShapeType.Polyline]: Polyline,
12 14
   [ShapeType.Rectangle]: Rectangle,
13 15
 }
16
+
17
+export default shapes

+ 9
- 1
lib/shapes/polyline.tsx Visa fil

@@ -1,6 +1,7 @@
1 1
 import { v4 as uuid } from "uuid"
2 2
 import * as vec from "utils/vec"
3 3
 import { BaseLibShape, PolylineShape, ShapeType } from "types"
4
+import { boundsCache } from "./index"
4 5
 
5 6
 const Polyline: BaseLibShape<ShapeType.Polyline> = {
6 7
   create(props): PolylineShape {
@@ -23,6 +24,10 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
23 24
   },
24 25
 
25 26
   getBounds(shape) {
27
+    if (boundsCache.has(shape)) {
28
+      return boundsCache.get(shape)
29
+    }
30
+
26 31
     let minX = 0
27 32
     let minY = 0
28 33
     let maxX = 0
@@ -35,7 +40,7 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
35 40
       maxY = Math.max(y, maxY)
36 41
     }
37 42
 
38
-    return {
43
+    const bounds = {
39 44
       minX: minX + shape.point[0],
40 45
       minY: minY + shape.point[1],
41 46
       maxX: maxX + shape.point[0],
@@ -43,6 +48,9 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
43 48
       width: maxX - minX,
44 49
       height: maxY - minY,
45 50
     }
51
+
52
+    boundsCache.set(shape, bounds)
53
+    return bounds
46 54
   },
47 55
 
48 56
   hitTest(shape) {

+ 9
- 1
lib/shapes/rectangle.tsx Visa fil

@@ -1,6 +1,7 @@
1 1
 import { v4 as uuid } from "uuid"
2 2
 import * as vec from "utils/vec"
3 3
 import { BaseLibShape, RectangleShape, ShapeType } from "types"
4
+import { boundsCache } from "./index"
4 5
 
5 6
 const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
6 7
   create(props): RectangleShape {
@@ -23,12 +24,16 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
23 24
   },
24 25
 
25 26
   getBounds(shape) {
27
+    if (boundsCache.has(shape)) {
28
+      return boundsCache.get(shape)
29
+    }
30
+
26 31
     const {
27 32
       point: [x, y],
28 33
       size: [width, height],
29 34
     } = shape
30 35
 
31
-    return {
36
+    const bounds = {
32 37
       minX: x,
33 38
       maxX: x + width,
34 39
       minY: y,
@@ -36,6 +41,9 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
36 41
       width,
37 42
       height,
38 43
     }
44
+
45
+    boundsCache.set(shape, bounds)
46
+    return bounds
39 47
   },
40 48
 
41 49
   hitTest(shape) {

+ 1
- 0
package.json Visa fil

@@ -11,6 +11,7 @@
11 11
     "@state-designer/react": "^1.7.1",
12 12
     "@stitches/react": "^0.1.9",
13 13
     "@types/uuid": "^8.3.0",
14
+    "framer-motion": "^4.1.16",
14 15
     "next": "10.2.0",
15 16
     "perfect-freehand": "^0.4.7",
16 17
     "react": "17.0.2",

+ 39
- 2
state/state.ts Visa fil

@@ -1,8 +1,9 @@
1 1
 import { createSelectorHook, createState } from "@state-designer/react"
2
-import { clamp, screenToWorld } from "utils/utils"
2
+import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
3 3
 import * as vec from "utils/vec"
4
-import { Data } from "types"
4
+import { Bounds, Data, Shape, ShapeType } from "types"
5 5
 import { defaultDocument } from "./data"
6
+import Shapes from "lib/shapes"
6 7
 import * as Sessions from "./sessions"
7 8
 
8 9
 const initialData: Data = {
@@ -131,6 +132,42 @@ const state = createState({
131 132
     selectedIds(data) {
132 133
       return new Set(data.selectedIds)
133 134
     },
135
+    selectedBounds(data) {
136
+      const {
137
+        selectedIds,
138
+        currentPageId,
139
+        document: { pages },
140
+      } = data
141
+
142
+      return getCommonBounds(
143
+        ...Array.from(selectedIds.values())
144
+          .map((id) => {
145
+            const shape = pages[currentPageId].shapes[id]
146
+
147
+            switch (shape.type) {
148
+              case ShapeType.Dot: {
149
+                return Shapes[shape.type].getBounds(shape)
150
+              }
151
+              case ShapeType.Circle: {
152
+                return Shapes[shape.type].getBounds(shape)
153
+              }
154
+              case ShapeType.Line: {
155
+                return Shapes[shape.type].getBounds(shape)
156
+              }
157
+              case ShapeType.Polyline: {
158
+                return Shapes[shape.type].getBounds(shape)
159
+              }
160
+              case ShapeType.Rectangle: {
161
+                return Shapes[shape.type].getBounds(shape)
162
+              }
163
+              default: {
164
+                return null
165
+              }
166
+            }
167
+          })
168
+          .filter(Boolean)
169
+      )
170
+    },
134 171
   },
135 172
 })
136 173
 

+ 7
- 0
styles/stitches.config.ts Visa fil

@@ -10,6 +10,8 @@ const { styled, global, css, theme, getCssString } = createCss({
10 10
       brushStroke: "rgba(0,0,0,.5)",
11 11
       hint: "rgba(66, 133, 244, 0.200)",
12 12
       selected: "rgba(66, 133, 244, 1.000)",
13
+      bounds: "rgba(65, 132, 244, 1.000)",
14
+      boundsBg: "rgba(65, 132, 244, 0.100)",
13 15
     },
14 16
     space: {},
15 17
     fontSizes: {
@@ -33,6 +35,11 @@ const { styled, global, css, theme, getCssString } = createCss({
33 35
     zIndices: {},
34 36
     transitions: {},
35 37
   },
38
+  utils: {
39
+    zStrokeWidth: () => (value: number) => ({
40
+      strokeWidth: `calc(${value}px / var(--camera-zoom))`,
41
+    }),
42
+  },
36 43
 })
37 44
 
38 45
 const light = theme({})

+ 34
- 1
utils/utils.ts Visa fil

@@ -1,4 +1,4 @@
1
-import { Data } from "types"
1
+import { Data, Bounds } from "types"
2 2
 import * as svg from "./svg"
3 3
 import * as vec from "./vec"
4 4
 
@@ -6,6 +6,39 @@ export function screenToWorld(point: number[], data: Data) {
6 6
   return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
7 7
 }
8 8
 
9
+/**
10
+ * Get a bounding box that includes two bounding boxes.
11
+ * @param a Bounding box
12
+ * @param b Bounding box
13
+ * @returns
14
+ */
15
+export function getExpandedBounds(a: Bounds, b: Bounds) {
16
+  const minX = Math.min(a.minX, b.minX),
17
+    minY = Math.min(a.minY, b.minY),
18
+    maxX = Math.max(a.maxX, b.maxX),
19
+    maxY = Math.max(a.maxY, b.maxY),
20
+    width = Math.abs(maxX - minX),
21
+    height = Math.abs(maxY - minY)
22
+
23
+  return { minX, minY, maxX, maxY, width, height }
24
+}
25
+
26
+/**
27
+ * Get the common bounds of a group of bounds.
28
+ * @returns
29
+ */
30
+export function getCommonBounds(...b: Bounds[]) {
31
+  if (b.length < 2) return b[0]
32
+
33
+  let bounds = b[0]
34
+
35
+  for (let i = 1; i < b.length; i++) {
36
+    bounds = getExpandedBounds(bounds, b[i])
37
+  }
38
+
39
+  return bounds
40
+}
41
+
9 42
 export function getBoundsFromPoints(a: number[], b: number[]) {
10 43
   const minX = Math.min(a[0], b[0])
11 44
   const maxX = Math.max(a[0], b[0])

+ 56
- 1
yarn.lock Visa fil

@@ -939,6 +939,18 @@
939 939
     exec-sh "^0.3.2"
940 940
     minimist "^1.2.0"
941 941
 
942
+"@emotion/is-prop-valid@^0.8.2":
943
+  version "0.8.8"
944
+  resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
945
+  integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
946
+  dependencies:
947
+    "@emotion/memoize" "0.7.4"
948
+
949
+"@emotion/memoize@0.7.4":
950
+  version "0.7.4"
951
+  resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
952
+  integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
953
+
942 954
 "@hapi/accept@5.0.1":
943 955
   version "5.0.1"
944 956
   resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.1.tgz#068553e867f0f63225a506ed74e899441af53e10"
@@ -3402,6 +3414,26 @@ fragment-cache@^0.2.1:
3402 3414
   dependencies:
3403 3415
     map-cache "^0.2.2"
3404 3416
 
3417
+framer-motion@^4.1.16:
3418
+  version "4.1.16"
3419
+  resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.16.tgz#dc715334847d0a146acf47f61019222d0d1c46c9"
3420
+  integrity sha512-sEc3UI3oncwE+RUzdd86TxbmpEaX/Ki/T0AmFYSsbxEqGZ3feLvzGL7BJlkhERIyyuAC9+OzI4BnhJM0GSUAMA==
3421
+  dependencies:
3422
+    framesync "5.3.0"
3423
+    hey-listen "^1.0.8"
3424
+    popmotion "9.3.6"
3425
+    style-value-types "4.1.4"
3426
+    tslib "^2.1.0"
3427
+  optionalDependencies:
3428
+    "@emotion/is-prop-valid" "^0.8.2"
3429
+
3430
+framesync@5.3.0:
3431
+  version "5.3.0"
3432
+  resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.3.0.tgz#0ecfc955e8f5a6ddc8fdb0cc024070947e1a0d9b"
3433
+  integrity sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==
3434
+  dependencies:
3435
+    tslib "^2.1.0"
3436
+
3405 3437
 fs-extra@8.1.0:
3406 3438
   version "8.1.0"
3407 3439
   resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
@@ -3652,6 +3684,11 @@ he@1.2.0:
3652 3684
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
3653 3685
   integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
3654 3686
 
3687
+hey-listen@^1.0.8:
3688
+  version "1.0.8"
3689
+  resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
3690
+  integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
3691
+
3655 3692
 hmac-drbg@^1.0.1:
3656 3693
   version "1.0.1"
3657 3694
   resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@@ -5622,6 +5659,16 @@ pnp-webpack-plugin@1.6.4:
5622 5659
   dependencies:
5623 5660
     ts-pnp "^1.1.6"
5624 5661
 
5662
+popmotion@9.3.6:
5663
+  version "9.3.6"
5664
+  resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.6.tgz#b5236fa28f242aff3871b9e23721f093133248d1"
5665
+  integrity sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==
5666
+  dependencies:
5667
+    framesync "5.3.0"
5668
+    hey-listen "^1.0.8"
5669
+    style-value-types "4.1.4"
5670
+    tslib "^2.1.0"
5671
+
5625 5672
 posix-character-classes@^0.1.0:
5626 5673
   version "0.1.1"
5627 5674
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -6734,6 +6781,14 @@ strip-json-comments@^3.0.1:
6734 6781
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
6735 6782
   integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
6736 6783
 
6784
+style-value-types@4.1.4:
6785
+  version "4.1.4"
6786
+  resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-4.1.4.tgz#80f37cb4fb024d6394087403dfb275e8bb627e75"
6787
+  integrity sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==
6788
+  dependencies:
6789
+    hey-listen "^1.0.8"
6790
+    tslib "^2.1.0"
6791
+
6737 6792
 styled-jsx@3.3.2:
6738 6793
   version "3.3.2"
6739 6794
   resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018"
@@ -7051,7 +7106,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
7051 7106
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
7052 7107
   integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
7053 7108
 
7054
-tslib@^2.0.3:
7109
+tslib@^2.0.3, tslib@^2.1.0:
7055 7110
   version "2.2.0"
7056 7111
   resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
7057 7112
   integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==

Laddar…
Avbryt
Spara