Parcourir la source

finishes rotation

main
Steve Ruiz il y a 4 ans
Parent
révision
1ece606db0

+ 9
- 8
components/canvas/bounds-bg.tsx Voir le fichier

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

+ 12
- 11
components/canvas/bounds.tsx Voir le fichier

@@ -6,15 +6,19 @@ import { TransformCorner, TransformEdge } from "types"
6 6
 import { lerp } from "utils/utils"
7 7
 
8 8
 export default function Bounds() {
9
-  const zoom = useSelector((state) => state.data.camera.zoom)
10
-  const bounds = useSelector((state) => state.values.selectedBounds)
11
-  const singleSelection = useSelector((s) => {
9
+  const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
10
+  const zoom = useSelector((s) => s.data.camera.zoom)
11
+  const bounds = useSelector((s) => s.values.selectedBounds)
12
+
13
+  const rotation = useSelector((s) => {
12 14
     if (s.data.selectedIds.size === 1) {
15
+      const { shapes } = s.data.document.pages[s.data.currentPageId]
13 16
       const selected = Array.from(s.data.selectedIds.values())[0]
14
-      return s.data.document.pages[s.data.currentPageId].shapes[selected]
17
+      return shapes[selected].rotation
18
+    } else {
19
+      return 0
15 20
     }
16 21
   })
17
-  const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
18 22
 
19 23
   if (!bounds) return null
20 24
 
@@ -26,12 +30,9 @@ export default function Bounds() {
26 30
   return (
27 31
     <g
28 32
       pointerEvents={isBrushing ? "none" : "all"}
29
-      transform={
30
-        singleSelection &&
31
-        `rotate(${singleSelection.rotation * (180 / Math.PI)},${
32
-          minX + width / 2
33
-        }, ${minY + width / 2})`
34
-      }
33
+      transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
34
+        minY + height / 2
35
+      })`}
35 36
     >
36 37
       <StyledBounds
37 38
         x={minX}

+ 17
- 17
lib/shapes/circle.tsx Voir le fichier

@@ -5,6 +5,7 @@ import { createShape } from "./index"
5 5
 import { boundsContained } from "utils/bounds"
6 6
 import { intersectCircleBounds } from "utils/intersections"
7 7
 import { pointInCircle } from "utils/hitTests"
8
+import { translateBounds } from "utils/utils"
8 9
 
9 10
 const circle = createShape<CircleShape>({
10 11
   boundsCache: new WeakMap([]),
@@ -33,27 +34,26 @@ const circle = createShape<CircleShape>({
33 34
   },
34 35
 
35 36
   getBounds(shape) {
36
-    if (this.boundsCache.has(shape)) {
37
-      return this.boundsCache.get(shape)
38
-    }
37
+    if (!this.boundsCache.has(shape)) {
38
+      const { radius } = shape
39
+
40
+      const bounds = {
41
+        minX: 0,
42
+        maxX: radius * 2,
43
+        minY: 0,
44
+        maxY: radius * 2,
45
+        width: radius * 2,
46
+        height: radius * 2,
47
+      }
39 48
 
40
-    const {
41
-      point: [x, y],
42
-      radius,
43
-    } = shape
44
-
45
-    const bounds = {
46
-      minX: x,
47
-      maxX: x + radius * 2,
48
-      minY: y,
49
-      maxY: y + radius * 2,
50
-      width: radius * 2,
51
-      height: radius * 2,
49
+      this.boundsCache.set(shape, bounds)
52 50
     }
53 51
 
54
-    this.boundsCache.set(shape, bounds)
52
+    return translateBounds(this.boundsCache.get(shape), shape.point)
53
+  },
55 54
 
56
-    return bounds
55
+  getRotatedBounds(shape) {
56
+    return this.getBounds(shape)
57 57
   },
58 58
 
59 59
   getCenter(shape) {

+ 16
- 17
lib/shapes/dot.tsx Voir le fichier

@@ -6,6 +6,7 @@ import { boundsContained } from "utils/bounds"
6 6
 import { intersectCircleBounds } from "utils/intersections"
7 7
 import styled from "styles"
8 8
 import { DotCircle } from "components/canvas/misc"
9
+import { translateBounds } from "utils/utils"
9 10
 
10 11
 const dot = createShape<DotShape>({
11 12
   boundsCache: new WeakMap([]),
@@ -33,26 +34,24 @@ const dot = createShape<DotShape>({
33 34
   },
34 35
 
35 36
   getBounds(shape) {
36
-    if (this.boundsCache.has(shape)) {
37
-      return this.boundsCache.get(shape)
37
+    if (!this.boundsCache.has(shape)) {
38
+      const bounds = {
39
+        minX: 0,
40
+        maxX: 1,
41
+        minY: 0,
42
+        maxY: 1,
43
+        width: 1,
44
+        height: 1,
45
+      }
46
+
47
+      this.boundsCache.set(shape, bounds)
38 48
     }
39 49
 
40
-    const {
41
-      point: [x, y],
42
-    } = shape
43
-
44
-    const bounds = {
45
-      minX: x,
46
-      maxX: x + 1,
47
-      minY: y,
48
-      maxY: y + 1,
49
-      width: 1,
50
-      height: 1,
51
-    }
52
-
53
-    this.boundsCache.set(shape, bounds)
50
+    return translateBounds(this.boundsCache.get(shape), shape.point)
51
+  },
54 52
 
55
-    return bounds
53
+  getRotatedBounds(shape) {
54
+    return this.getBounds(shape)
56 55
   },
57 56
 
58 57
   getCenter(shape) {

+ 18
- 19
lib/shapes/ellipse.tsx Voir le fichier

@@ -5,6 +5,7 @@ import { createShape } from "./index"
5 5
 import { boundsContained } from "utils/bounds"
6 6
 import { intersectEllipseBounds } from "utils/intersections"
7 7
 import { pointInEllipse } from "utils/hitTests"
8
+import { translateBounds } from "utils/utils"
8 9
 
9 10
 const ellipse = createShape<EllipseShape>({
10 11
   boundsCache: new WeakMap([]),
@@ -36,28 +37,26 @@ const ellipse = createShape<EllipseShape>({
36 37
   },
37 38
 
38 39
   getBounds(shape) {
39
-    if (this.boundsCache.has(shape)) {
40
-      return this.boundsCache.get(shape)
40
+    if (!this.boundsCache.has(shape)) {
41
+      const { radiusX, radiusY } = shape
42
+
43
+      const bounds = {
44
+        minX: 0,
45
+        maxX: radiusX * 2,
46
+        minY: 0,
47
+        maxY: radiusY * 2,
48
+        width: radiusX * 2,
49
+        height: radiusY * 2,
50
+      }
51
+
52
+      this.boundsCache.set(shape, bounds)
41 53
     }
42 54
 
43
-    const {
44
-      point: [x, y],
45
-      radiusX,
46
-      radiusY,
47
-    } = shape
48
-
49
-    const bounds = {
50
-      minX: x,
51
-      maxX: x + radiusX * 2,
52
-      minY: y,
53
-      maxY: y + radiusY * 2,
54
-      width: radiusX * 2,
55
-      height: radiusY * 2,
56
-    }
57
-
58
-    this.boundsCache.set(shape, bounds)
55
+    return translateBounds(this.boundsCache.get(shape), shape.point)
56
+  },
59 57
 
60
-    return bounds
58
+  getRotatedBounds(shape) {
59
+    return this.getBounds(shape)
61 60
   },
62 61
 
63 62
   getCenter(shape) {

+ 3
- 0
lib/shapes/index.tsx Voir le fichier

@@ -36,6 +36,9 @@ export interface ShapeUtility<K extends Shape> {
36 36
   // Get the bounds of the a shape.
37 37
   getBounds(this: ShapeUtility<K>, shape: K): Bounds
38 38
 
39
+  // Get the routated bounds of the a shape.
40
+  getRotatedBounds(this: ShapeUtility<K>, shape: K): Bounds
41
+
39 42
   // Get the center of the shape
40 43
   getCenter(this: ShapeUtility<K>, shape: K): number[]
41 44
 

+ 16
- 17
lib/shapes/line.tsx Voir le fichier

@@ -5,6 +5,7 @@ import { createShape } from "./index"
5 5
 import { boundsContained } from "utils/bounds"
6 6
 import { intersectCircleBounds } from "utils/intersections"
7 7
 import { DotCircle } from "components/canvas/misc"
8
+import { translateBounds } from "utils/utils"
8 9
 
9 10
 const line = createShape<LineShape>({
10 11
   boundsCache: new WeakMap([]),
@@ -41,26 +42,24 @@ const line = createShape<LineShape>({
41 42
   },
42 43
 
43 44
   getBounds(shape) {
44
-    if (this.boundsCache.has(shape)) {
45
-      return this.boundsCache.get(shape)
45
+    if (!this.boundsCache.has(shape)) {
46
+      const bounds = {
47
+        minX: 0,
48
+        maxX: 1,
49
+        minY: 0,
50
+        maxY: 1,
51
+        width: 1,
52
+        height: 1,
53
+      }
54
+
55
+      this.boundsCache.set(shape, bounds)
46 56
     }
47 57
 
48
-    const {
49
-      point: [x, y],
50
-    } = shape
51
-
52
-    const bounds = {
53
-      minX: x,
54
-      maxX: x + 1,
55
-      minY: y,
56
-      maxY: y + 1,
57
-      width: 1,
58
-      height: 1,
59
-    }
60
-
61
-    this.boundsCache.set(shape, bounds)
58
+    return translateBounds(this.boundsCache.get(shape), shape.point)
59
+  },
62 60
 
63
-    return bounds
61
+  getRotatedBounds(shape) {
62
+    return this.getBounds(shape)
64 63
   },
65 64
 
66 65
   getCenter(shape) {

+ 29
- 33
lib/shapes/polyline.tsx Voir le fichier

@@ -3,7 +3,12 @@ import * as vec from "utils/vec"
3 3
 import { PolylineShape, ShapeType } from "types"
4 4
 import { createShape } from "./index"
5 5
 import { intersectPolylineBounds } from "utils/intersections"
6
-import { boundsCollide, boundsContained } from "utils/bounds"
6
+import {
7
+  boundsCollide,
8
+  boundsContained,
9
+  boundsContainPolygon,
10
+} from "utils/bounds"
11
+import { getBoundsFromPoints, translateBounds } from "utils/utils"
7 12
 
8 13
 const polyline = createShape<PolylineShape>({
9 14
   boundsCache: new WeakMap([]),
@@ -29,33 +34,16 @@ const polyline = createShape<PolylineShape>({
29 34
   },
30 35
 
31 36
   getBounds(shape) {
32
-    if (this.boundsCache.has(shape)) {
33
-      return this.boundsCache.get(shape)
37
+    if (!this.boundsCache.has(shape)) {
38
+      const bounds = getBoundsFromPoints(shape.points)
39
+      this.boundsCache.set(shape, bounds)
34 40
     }
35 41
 
36
-    let minX = 0
37
-    let minY = 0
38
-    let maxX = 0
39
-    let maxY = 0
40
-
41
-    for (let [x, y] of shape.points) {
42
-      minX = Math.min(x, minX)
43
-      minY = Math.min(y, minY)
44
-      maxX = Math.max(x, maxX)
45
-      maxY = Math.max(y, maxY)
46
-    }
47
-
48
-    const bounds = {
49
-      minX: minX + shape.point[0],
50
-      minY: minY + shape.point[1],
51
-      maxX: maxX + shape.point[0],
52
-      maxY: maxY + shape.point[1],
53
-      width: maxX - minX,
54
-      height: maxY - minY,
55
-    }
42
+    return translateBounds(this.boundsCache.get(shape), shape.point)
43
+  },
56 44
 
57
-    this.boundsCache.set(shape, bounds)
58
-    return bounds
45
+  getRotatedBounds(shape) {
46
+    return this.getBounds(shape)
59 47
   },
60 48
 
61 49
   getCenter(shape) {
@@ -78,15 +66,23 @@ const polyline = createShape<PolylineShape>({
78 66
     return false
79 67
   },
80 68
 
81
-  hitTestBounds(this, shape, bounds) {
82
-    const shapeBounds = this.getBounds(shape)
69
+  hitTestBounds(this, shape, brushBounds) {
70
+    const b = this.getBounds(shape)
71
+    const center = [b.minX + b.width / 2, b.minY + b.height / 2]
72
+
73
+    const rotatedCorners = [
74
+      [b.minX, b.minY],
75
+      [b.maxX, b.minY],
76
+      [b.maxX, b.maxY],
77
+      [b.minX, b.maxY],
78
+    ].map((point) => vec.rotWith(point, center, shape.rotation))
79
+
83 80
     return (
84
-      boundsContained(shapeBounds, bounds) ||
85
-      (boundsCollide(shapeBounds, bounds) &&
86
-        intersectPolylineBounds(
87
-          shape.points.map((point) => vec.add(point, shape.point)),
88
-          bounds
89
-        ).length > 0)
81
+      boundsContainPolygon(brushBounds, rotatedCorners) ||
82
+      intersectPolylineBounds(
83
+        shape.points.map((point) => vec.add(point, shape.point)),
84
+        brushBounds
85
+      ).length > 0
90 86
     )
91 87
   },
92 88
 

+ 17
- 18
lib/shapes/ray.tsx Voir le fichier

@@ -5,6 +5,7 @@ import { createShape } from "./index"
5 5
 import { boundsContained } from "utils/bounds"
6 6
 import { intersectCircleBounds } from "utils/intersections"
7 7
 import { DotCircle } from "components/canvas/misc"
8
+import { translateBounds } from "utils/utils"
8 9
 
9 10
 const ray = createShape<RayShape>({
10 11
   boundsCache: new WeakMap([]),
@@ -40,27 +41,25 @@ const ray = createShape<RayShape>({
40 41
     )
41 42
   },
42 43
 
43
-  getBounds(shape) {
44
-    if (this.boundsCache.has(shape)) {
45
-      return this.boundsCache.get(shape)
46
-    }
44
+  getRotatedBounds(shape) {
45
+    return this.getBounds(shape)
46
+  },
47 47
 
48
-    const {
49
-      point: [x, y],
50
-    } = shape
51
-
52
-    const bounds = {
53
-      minX: x,
54
-      maxX: x + 8,
55
-      minY: y,
56
-      maxY: y + 8,
57
-      width: 8,
58
-      height: 8,
48
+  getBounds(shape) {
49
+    if (!this.boundsCache.has(shape)) {
50
+      const bounds = {
51
+        minX: 0,
52
+        maxX: 1,
53
+        minY: 0,
54
+        maxY: 1,
55
+        width: 1,
56
+        height: 1,
57
+      }
58
+
59
+      this.boundsCache.set(shape, bounds)
59 60
     }
60 61
 
61
-    this.boundsCache.set(shape, bounds)
62
-
63
-    return bounds
62
+    return translateBounds(this.boundsCache.get(shape), shape.point)
64 63
   },
65 64
 
66 65
   getCenter(shape) {

+ 41
- 22
lib/shapes/rectangle.tsx Voir le fichier

@@ -2,7 +2,8 @@ import { v4 as uuid } from "uuid"
2 2
 import * as vec from "utils/vec"
3 3
 import { RectangleShape, ShapeType } from "types"
4 4
 import { createShape } from "./index"
5
-import { boundsContained, boundsCollide } from "utils/bounds"
5
+import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
6
+import { getBoundsFromPoints, rotateBounds, translateBounds } from "utils/utils"
6 7
 
7 8
 const rectangle = createShape<RectangleShape>({
8 9
   boundsCache: new WeakMap([]),
@@ -31,31 +32,40 @@ const rectangle = createShape<RectangleShape>({
31 32
   },
32 33
 
33 34
   getBounds(shape) {
34
-    if (this.boundsCache.has(shape)) {
35
-      return this.boundsCache.get(shape)
35
+    if (!this.boundsCache.has(shape)) {
36
+      const [width, height] = shape.size
37
+      const bounds = {
38
+        minX: 0,
39
+        maxX: width,
40
+        minY: 0,
41
+        maxY: height,
42
+        width,
43
+        height,
44
+      }
45
+
46
+      this.boundsCache.set(shape, bounds)
36 47
     }
37 48
 
38
-    const {
39
-      point: [x, y],
40
-      size: [width, height],
41
-    } = shape
42
-
43
-    const bounds = {
44
-      minX: x,
45
-      maxX: x + width,
46
-      minY: y,
47
-      maxY: y + height,
48
-      width,
49
-      height,
50
-    }
49
+    return translateBounds(this.boundsCache.get(shape), shape.point)
50
+  },
51
+
52
+  getRotatedBounds(shape) {
53
+    const b = this.getBounds(shape)
54
+    const center = [b.minX + b.width / 2, b.minY + b.height / 2]
51 55
 
52
-    this.boundsCache.set(shape, bounds)
56
+    // Rotate corners of the shape, then find the minimum among those points.
57
+    const rotatedCorners = [
58
+      [b.minX, b.minY],
59
+      [b.maxX, b.minY],
60
+      [b.maxX, b.maxY],
61
+      [b.minX, b.maxY],
62
+    ].map((point) => vec.rotWith(point, center, shape.rotation))
53 63
 
54
-    return bounds
64
+    return getBoundsFromPoints(rotatedCorners)
55 65
   },
56 66
 
57 67
   getCenter(shape) {
58
-    const bounds = this.getBounds(shape)
68
+    const bounds = this.getRotatedBounds(shape)
59 69
     return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
60 70
   },
61 71
 
@@ -64,10 +74,19 @@ const rectangle = createShape<RectangleShape>({
64 74
   },
65 75
 
66 76
   hitTestBounds(shape, brushBounds) {
67
-    const shapeBounds = this.getBounds(shape)
77
+    const b = this.getBounds(shape)
78
+    const center = [b.minX + b.width / 2, b.minY + b.height / 2]
79
+
80
+    const rotatedCorners = [
81
+      [b.minX, b.minY],
82
+      [b.maxX, b.minY],
83
+      [b.maxX, b.maxY],
84
+      [b.minX, b.maxY],
85
+    ].map((point) => vec.rotWith(point, center, shape.rotation))
86
+
68 87
     return (
69
-      boundsContained(shapeBounds, brushBounds) ||
70
-      boundsCollide(shapeBounds, brushBounds)
88
+      boundsContainPolygon(brushBounds, rotatedCorners) ||
89
+      boundsCollidePolygon(brushBounds, rotatedCorners)
71 90
     )
72 91
   },
73 92
 

+ 12
- 4
state/commands/rotate.ts Voir le fichier

@@ -16,16 +16,24 @@ export default function translateCommand(
16 16
       do(data) {
17 17
         const { shapes } = data.document.pages[after.currentPageId]
18 18
 
19
-        for (let { id, rotation } of after.shapes) {
20
-          shapes[id].rotation = rotation
19
+        for (let { id, point, rotation } of after.shapes) {
20
+          const shape = shapes[id]
21
+          shape.rotation = rotation
22
+          shape.point = point
21 23
         }
24
+
25
+        data.boundsRotation = after.boundsRotation
22 26
       },
23 27
       undo(data) {
24 28
         const { shapes } = data.document.pages[before.currentPageId]
25 29
 
26
-        for (let { id, rotation } of before.shapes) {
27
-          shapes[id].rotation = rotation
30
+        for (let { id, point, rotation } of before.shapes) {
31
+          const shape = shapes[id]
32
+          shape.rotation = rotation
33
+          shape.point = point
28 34
         }
35
+
36
+        data.boundsRotation = before.boundsRotation
29 37
       },
30 38
     })
31 39
   )

+ 1
- 1
state/sessions/brush-session.ts Voir le fichier

@@ -25,7 +25,7 @@ export default class BrushSession extends BaseSession {
25 25
   update = (data: Data, point: number[]) => {
26 26
     const { origin, snapshot } = this
27 27
 
28
-    const brushBounds = getBoundsFromPoints(origin, point)
28
+    const brushBounds = getBoundsFromPoints([origin, point])
29 29
 
30 30
     for (let { test, id } of snapshot.shapes) {
31 31
       if (test(brushBounds)) {

+ 35
- 14
state/sessions/rotate-session.ts Voir le fichier

@@ -18,24 +18,34 @@ export default class RotateSession extends BaseSession {
18 18
   }
19 19
 
20 20
   update(data: Data, point: number[]) {
21
-    const { currentPageId, center, shapes } = this.snapshot
21
+    const { currentPageId, boundsCenter, shapes } = this.snapshot
22 22
     const { document } = data
23 23
 
24
-    const a1 = vec.angle(center, this.origin)
25
-    const a2 = vec.angle(center, point)
24
+    const a1 = vec.angle(boundsCenter, this.origin)
25
+    const a2 = vec.angle(boundsCenter, point)
26 26
 
27
-    for (let { id, rotation } of shapes) {
27
+    data.boundsRotation =
28
+      (this.snapshot.boundsRotation + (a2 - a1)) % (Math.PI * 2)
29
+
30
+    for (let { id, center, offset, rotation } of shapes) {
28 31
       const shape = document.pages[currentPageId].shapes[id]
29 32
       shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
33
+      const newCenter = vec.rotWith(
34
+        center,
35
+        boundsCenter,
36
+        (a2 - a1) % (Math.PI * 2)
37
+      )
38
+      shape.point = vec.sub(newCenter, offset)
30 39
     }
31 40
   }
32 41
 
33 42
   cancel(data: Data) {
34 43
     const { document } = data
35 44
 
36
-    for (let shape of this.snapshot.shapes) {
37
-      document.pages[this.snapshot.currentPageId].shapes[shape.id].rotation =
38
-        shape.rotation
45
+    for (let { id, point, rotation } of this.snapshot.shapes) {
46
+      const shape = document.pages[this.snapshot.currentPageId].shapes[id]
47
+      shape.rotation = rotation
48
+      shape.point = point
39 49
     }
40 50
   }
41 51
 
@@ -46,9 +56,10 @@ export default class RotateSession extends BaseSession {
46 56
 
47 57
 export function getRotateSnapshot(data: Data) {
48 58
   const {
59
+    boundsRotation,
49 60
     selectedIds,
50
-    document: { pages },
51 61
     currentPageId,
62
+    document: { pages },
52 63
   } = current(data)
53 64
 
54 65
   const shapes = Array.from(selectedIds.values()).map(
@@ -63,18 +74,28 @@ export function getRotateSnapshot(data: Data) {
63 74
   // The common (exterior) bounds of the selected shapes
64 75
   const bounds = getCommonBounds(...Object.values(shapesBounds))
65 76
 
66
-  const center = [
77
+  const boundsCenter = [
67 78
     bounds.minX + bounds.width / 2,
68 79
     bounds.minY + bounds.height / 2,
69 80
   ]
70 81
 
71 82
   return {
72 83
     currentPageId,
73
-    center,
74
-    shapes: shapes.map(({ id, rotation }) => ({
75
-      id,
76
-      rotation,
77
-    })),
84
+    boundsCenter,
85
+    boundsRotation,
86
+    shapes: shapes.map(({ id, point, rotation }) => {
87
+      const bounds = shapesBounds[id]
88
+      const offset = [bounds.width / 2, bounds.height / 2]
89
+      const center = vec.add(offset, [bounds.minX, bounds.minY])
90
+
91
+      return {
92
+        id,
93
+        point,
94
+        rotation,
95
+        offset,
96
+        center,
97
+      }
98
+    }),
78 99
   }
79 100
 }
80 101
 

+ 11
- 3
state/state.ts Voir le fichier

@@ -29,6 +29,7 @@ const initialData: Data = {
29 29
     zoom: 1,
30 30
   },
31 31
   brush: undefined,
32
+  boundsRotation: 0,
32 33
   pointedId: null,
33 34
   hoveredId: null,
34 35
   selectedIds: new Set([]),
@@ -180,6 +181,7 @@ const state = createState({
180 181
             brushSelecting: {
181 182
               onEnter: [
182 183
                 { unless: "isPressingShiftKey", do: "clearSelectedIds" },
184
+                "clearBoundsRotation",
183 185
                 "startBrushSession",
184 186
               ],
185 187
               on: {
@@ -708,6 +710,10 @@ const state = createState({
708 710
     restoreSavedData(data) {
709 711
       history.load(data)
710 712
     },
713
+
714
+    clearBoundsRotation(data) {
715
+      data.boundsRotation = 0
716
+    },
711 717
   },
712 718
   values: {
713 719
     selectedIds(data) {
@@ -726,12 +732,14 @@ const state = createState({
726 732
 
727 733
       if (selectedIds.size === 0) return null
728 734
 
729
-      if (selectedIds.size === 1 && !getShapeUtils(shapes[0]).canTransform) {
730
-        return null
735
+      if (selectedIds.size === 1) {
736
+        const shapeUtils = getShapeUtils(shapes[0])
737
+        if (!shapeUtils.canTransform) return null
738
+        return shapeUtils.getBounds(shapes[0])
731 739
       }
732 740
 
733 741
       return getCommonBounds(
734
-        ...shapes.map((shape) => getShapeUtils(shape).getBounds(shape))
742
+        ...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
735 743
       )
736 744
     },
737 745
   },

+ 5
- 0
types.ts Voir le fichier

@@ -18,6 +18,7 @@ export interface Data {
18 18
     zoom: number
19 19
   }
20 20
   brush?: Bounds
21
+  boundsRotation: number
21 22
   selectedIds: Set<string>
22 23
   pointedId?: string
23 24
   hoveredId?: string
@@ -168,6 +169,10 @@ export interface Bounds {
168 169
   height: number
169 170
 }
170 171
 
172
+export interface RotatedBounds extends Bounds {
173
+  rotation: number
174
+}
175
+
171 176
 export interface ShapeBounds extends Bounds {
172 177
   id: string
173 178
 }

+ 21
- 0
utils/bounds.ts Voir le fichier

@@ -1,4 +1,8 @@
1 1
 import { Bounds } from "types"
2
+import {
3
+  intersectPolygonBounds,
4
+  intersectPolylineBounds,
5
+} from "./intersections"
2 6
 
3 7
 /**
4 8
  * Get whether two bounds collide.
@@ -37,6 +41,23 @@ export function boundsContained(a: Bounds, b: Bounds) {
37 41
   return boundsContain(b, a)
38 42
 }
39 43
 
44
+/**
45
+ * Get whether a set of points are all contained by a bounding box.
46
+ * @returns
47
+ */
48
+export function boundsContainPolygon(a: Bounds, points: number[][]) {
49
+  return points.every((point) => pointInBounds(point, a))
50
+}
51
+
52
+/**
53
+ * Get whether a polygon collides a bounding box.
54
+ * @param points
55
+ * @param b
56
+ */
57
+export function boundsCollidePolygon(a: Bounds, points: number[][]) {
58
+  return intersectPolygonBounds(points, a).length > 0
59
+}
60
+
40 61
 /**
41 62
  * Get whether two bounds are identical.
42 63
  * @param a Bounds

+ 18
- 0
utils/intersections.ts Voir le fichier

@@ -342,3 +342,21 @@ export function intersectPolylineBounds(points: number[][], bounds: Bounds) {
342 342
 
343 343
   return intersections
344 344
 }
345
+
346
+export function intersectPolygonBounds(points: number[][], bounds: Bounds) {
347
+  const { minX, minY, width, height } = bounds
348
+  const intersections: Intersection[] = []
349
+
350
+  for (let i = 1; i < points.length + 1; i++) {
351
+    intersections.push(
352
+      ...intersectRectangleLineSegment(
353
+        [minX, minY],
354
+        [width, height],
355
+        points[i - 1],
356
+        points[i % points.length]
357
+      )
358
+    )
359
+  }
360
+
361
+  return intersections
362
+}

+ 73
- 15
utils/utils.ts Voir le fichier

@@ -41,21 +41,21 @@ export function getCommonBounds(...b: Bounds[]) {
41 41
   return bounds
42 42
 }
43 43
 
44
-export function getBoundsFromPoints(a: number[], b: number[]) {
45
-  const minX = Math.min(a[0], b[0])
46
-  const maxX = Math.max(a[0], b[0])
47
-  const minY = Math.min(a[1], b[1])
48
-  const maxY = Math.max(a[1], b[1])
49
-
50
-  return {
51
-    minX,
52
-    maxX,
53
-    minY,
54
-    maxY,
55
-    width: maxX - minX,
56
-    height: maxY - minY,
57
-  }
58
-}
44
+// export function getBoundsFromPoints(a: number[], b: number[]) {
45
+//   const minX = Math.min(a[0], b[0])
46
+//   const maxX = Math.max(a[0], b[0])
47
+//   const minY = Math.min(a[1], b[1])
48
+//   const maxY = Math.max(a[1], b[1])
49
+
50
+//   return {
51
+//     minX,
52
+//     maxX,
53
+//     minY,
54
+//     maxY,
55
+//     width: maxX - minX,
56
+//     height: maxY - minY,
57
+//   }
58
+// }
59 59
 
60 60
 // A helper for getting tangents.
61 61
 export function getCircleTangentToPoint(
@@ -962,3 +962,61 @@ export function vectorToPoint(point: number[] | Vector | undefined) {
962 962
   }
963 963
   return point
964 964
 }
965
+
966
+export function getBoundsFromPoints(points: number[][]): Bounds {
967
+  let minX = Infinity
968
+  let minY = Infinity
969
+  let maxX = -Infinity
970
+  let maxY = -Infinity
971
+
972
+  for (let [x, y] of points) {
973
+    minX = Math.min(x, minX)
974
+    minY = Math.min(y, minY)
975
+    maxX = Math.max(x, maxX)
976
+    maxY = Math.max(y, maxY)
977
+  }
978
+
979
+  return {
980
+    minX,
981
+    minY,
982
+    maxX,
983
+    maxY,
984
+    width: maxX - minX,
985
+    height: maxY - minY,
986
+  }
987
+}
988
+
989
+/**
990
+ * Move a bounding box without recalculating it.
991
+ * @param bounds
992
+ * @param delta
993
+ * @returns
994
+ */
995
+export function translateBounds(bounds: Bounds, delta: number[]) {
996
+  return {
997
+    minX: bounds.minX + delta[0],
998
+    minY: bounds.minY + delta[1],
999
+    maxX: bounds.maxX + delta[0],
1000
+    maxY: bounds.maxY + delta[1],
1001
+    width: bounds.width,
1002
+    height: bounds.height,
1003
+  }
1004
+}
1005
+
1006
+export function rotateBounds(
1007
+  bounds: Bounds,
1008
+  center: number[],
1009
+  rotation: number
1010
+) {
1011
+  const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
1012
+  const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
1013
+
1014
+  return {
1015
+    minX,
1016
+    minY,
1017
+    maxX,
1018
+    maxY,
1019
+    width: bounds.width,
1020
+    height: bounds.height,
1021
+  }
1022
+}

Chargement…
Annuler
Enregistrer