Browse Source

finishes rotation

main
Steve Ruiz 4 years ago
parent
commit
1ece606db0

+ 9
- 8
components/canvas/bounds-bg.tsx View File

6
 export default function BoundsBg() {
6
 export default function BoundsBg() {
7
   const rBounds = useRef<SVGRectElement>(null)
7
   const rBounds = useRef<SVGRectElement>(null)
8
   const bounds = useSelector((state) => state.values.selectedBounds)
8
   const bounds = useSelector((state) => state.values.selectedBounds)
9
-  const singleSelection = useSelector((s) => {
9
+
10
+  const rotation = useSelector((s) => {
10
     if (s.data.selectedIds.size === 1) {
11
     if (s.data.selectedIds.size === 1) {
12
+      const { shapes } = s.data.document.pages[s.data.currentPageId]
11
       const selected = Array.from(s.data.selectedIds.values())[0]
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
         rBounds.current.setPointerCapture(e.pointerId)
34
         rBounds.current.setPointerCapture(e.pointerId)
31
         state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
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 View File

6
 import { lerp } from "utils/utils"
6
 import { lerp } from "utils/utils"
7
 
7
 
8
 export default function Bounds() {
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
     if (s.data.selectedIds.size === 1) {
14
     if (s.data.selectedIds.size === 1) {
15
+      const { shapes } = s.data.document.pages[s.data.currentPageId]
13
       const selected = Array.from(s.data.selectedIds.values())[0]
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
   if (!bounds) return null
23
   if (!bounds) return null
20
 
24
 
26
   return (
30
   return (
27
     <g
31
     <g
28
       pointerEvents={isBrushing ? "none" : "all"}
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
       <StyledBounds
37
       <StyledBounds
37
         x={minX}
38
         x={minX}

+ 17
- 17
lib/shapes/circle.tsx View File

5
 import { boundsContained } from "utils/bounds"
5
 import { boundsContained } from "utils/bounds"
6
 import { intersectCircleBounds } from "utils/intersections"
6
 import { intersectCircleBounds } from "utils/intersections"
7
 import { pointInCircle } from "utils/hitTests"
7
 import { pointInCircle } from "utils/hitTests"
8
+import { translateBounds } from "utils/utils"
8
 
9
 
9
 const circle = createShape<CircleShape>({
10
 const circle = createShape<CircleShape>({
10
   boundsCache: new WeakMap([]),
11
   boundsCache: new WeakMap([]),
33
   },
34
   },
34
 
35
 
35
   getBounds(shape) {
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
   getCenter(shape) {
59
   getCenter(shape) {

+ 16
- 17
lib/shapes/dot.tsx View File

6
 import { intersectCircleBounds } from "utils/intersections"
6
 import { intersectCircleBounds } from "utils/intersections"
7
 import styled from "styles"
7
 import styled from "styles"
8
 import { DotCircle } from "components/canvas/misc"
8
 import { DotCircle } from "components/canvas/misc"
9
+import { translateBounds } from "utils/utils"
9
 
10
 
10
 const dot = createShape<DotShape>({
11
 const dot = createShape<DotShape>({
11
   boundsCache: new WeakMap([]),
12
   boundsCache: new WeakMap([]),
33
   },
34
   },
34
 
35
 
35
   getBounds(shape) {
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
   getCenter(shape) {
57
   getCenter(shape) {

+ 18
- 19
lib/shapes/ellipse.tsx View File

5
 import { boundsContained } from "utils/bounds"
5
 import { boundsContained } from "utils/bounds"
6
 import { intersectEllipseBounds } from "utils/intersections"
6
 import { intersectEllipseBounds } from "utils/intersections"
7
 import { pointInEllipse } from "utils/hitTests"
7
 import { pointInEllipse } from "utils/hitTests"
8
+import { translateBounds } from "utils/utils"
8
 
9
 
9
 const ellipse = createShape<EllipseShape>({
10
 const ellipse = createShape<EllipseShape>({
10
   boundsCache: new WeakMap([]),
11
   boundsCache: new WeakMap([]),
36
   },
37
   },
37
 
38
 
38
   getBounds(shape) {
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
   getCenter(shape) {
62
   getCenter(shape) {

+ 3
- 0
lib/shapes/index.tsx View File

36
   // Get the bounds of the a shape.
36
   // Get the bounds of the a shape.
37
   getBounds(this: ShapeUtility<K>, shape: K): Bounds
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
   // Get the center of the shape
42
   // Get the center of the shape
40
   getCenter(this: ShapeUtility<K>, shape: K): number[]
43
   getCenter(this: ShapeUtility<K>, shape: K): number[]
41
 
44
 

+ 16
- 17
lib/shapes/line.tsx View File

5
 import { boundsContained } from "utils/bounds"
5
 import { boundsContained } from "utils/bounds"
6
 import { intersectCircleBounds } from "utils/intersections"
6
 import { intersectCircleBounds } from "utils/intersections"
7
 import { DotCircle } from "components/canvas/misc"
7
 import { DotCircle } from "components/canvas/misc"
8
+import { translateBounds } from "utils/utils"
8
 
9
 
9
 const line = createShape<LineShape>({
10
 const line = createShape<LineShape>({
10
   boundsCache: new WeakMap([]),
11
   boundsCache: new WeakMap([]),
41
   },
42
   },
42
 
43
 
43
   getBounds(shape) {
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
   getCenter(shape) {
65
   getCenter(shape) {

+ 29
- 33
lib/shapes/polyline.tsx View File

3
 import { PolylineShape, ShapeType } from "types"
3
 import { PolylineShape, ShapeType } from "types"
4
 import { createShape } from "./index"
4
 import { createShape } from "./index"
5
 import { intersectPolylineBounds } from "utils/intersections"
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
 const polyline = createShape<PolylineShape>({
13
 const polyline = createShape<PolylineShape>({
9
   boundsCache: new WeakMap([]),
14
   boundsCache: new WeakMap([]),
29
   },
34
   },
30
 
35
 
31
   getBounds(shape) {
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
   getCenter(shape) {
49
   getCenter(shape) {
78
     return false
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
     return (
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 View File

5
 import { boundsContained } from "utils/bounds"
5
 import { boundsContained } from "utils/bounds"
6
 import { intersectCircleBounds } from "utils/intersections"
6
 import { intersectCircleBounds } from "utils/intersections"
7
 import { DotCircle } from "components/canvas/misc"
7
 import { DotCircle } from "components/canvas/misc"
8
+import { translateBounds } from "utils/utils"
8
 
9
 
9
 const ray = createShape<RayShape>({
10
 const ray = createShape<RayShape>({
10
   boundsCache: new WeakMap([]),
11
   boundsCache: new WeakMap([]),
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
   getCenter(shape) {
65
   getCenter(shape) {

+ 41
- 22
lib/shapes/rectangle.tsx View File

2
 import * as vec from "utils/vec"
2
 import * as vec from "utils/vec"
3
 import { RectangleShape, ShapeType } from "types"
3
 import { RectangleShape, ShapeType } from "types"
4
 import { createShape } from "./index"
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
 const rectangle = createShape<RectangleShape>({
8
 const rectangle = createShape<RectangleShape>({
8
   boundsCache: new WeakMap([]),
9
   boundsCache: new WeakMap([]),
31
   },
32
   },
32
 
33
 
33
   getBounds(shape) {
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
   getCenter(shape) {
67
   getCenter(shape) {
58
-    const bounds = this.getBounds(shape)
68
+    const bounds = this.getRotatedBounds(shape)
59
     return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
69
     return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
60
   },
70
   },
61
 
71
 
64
   },
74
   },
65
 
75
 
66
   hitTestBounds(shape, brushBounds) {
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
     return (
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 View File

16
       do(data) {
16
       do(data) {
17
         const { shapes } = data.document.pages[after.currentPageId]
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
       undo(data) {
27
       undo(data) {
24
         const { shapes } = data.document.pages[before.currentPageId]
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 View File

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

+ 35
- 14
state/sessions/rotate-session.ts View File

18
   }
18
   }
19
 
19
 
20
   update(data: Data, point: number[]) {
20
   update(data: Data, point: number[]) {
21
-    const { currentPageId, center, shapes } = this.snapshot
21
+    const { currentPageId, boundsCenter, shapes } = this.snapshot
22
     const { document } = data
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
       const shape = document.pages[currentPageId].shapes[id]
31
       const shape = document.pages[currentPageId].shapes[id]
29
       shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
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
   cancel(data: Data) {
42
   cancel(data: Data) {
34
     const { document } = data
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
 
56
 
47
 export function getRotateSnapshot(data: Data) {
57
 export function getRotateSnapshot(data: Data) {
48
   const {
58
   const {
59
+    boundsRotation,
49
     selectedIds,
60
     selectedIds,
50
-    document: { pages },
51
     currentPageId,
61
     currentPageId,
62
+    document: { pages },
52
   } = current(data)
63
   } = current(data)
53
 
64
 
54
   const shapes = Array.from(selectedIds.values()).map(
65
   const shapes = Array.from(selectedIds.values()).map(
63
   // The common (exterior) bounds of the selected shapes
74
   // The common (exterior) bounds of the selected shapes
64
   const bounds = getCommonBounds(...Object.values(shapesBounds))
75
   const bounds = getCommonBounds(...Object.values(shapesBounds))
65
 
76
 
66
-  const center = [
77
+  const boundsCenter = [
67
     bounds.minX + bounds.width / 2,
78
     bounds.minX + bounds.width / 2,
68
     bounds.minY + bounds.height / 2,
79
     bounds.minY + bounds.height / 2,
69
   ]
80
   ]
70
 
81
 
71
   return {
82
   return {
72
     currentPageId,
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 View File

29
     zoom: 1,
29
     zoom: 1,
30
   },
30
   },
31
   brush: undefined,
31
   brush: undefined,
32
+  boundsRotation: 0,
32
   pointedId: null,
33
   pointedId: null,
33
   hoveredId: null,
34
   hoveredId: null,
34
   selectedIds: new Set([]),
35
   selectedIds: new Set([]),
180
             brushSelecting: {
181
             brushSelecting: {
181
               onEnter: [
182
               onEnter: [
182
                 { unless: "isPressingShiftKey", do: "clearSelectedIds" },
183
                 { unless: "isPressingShiftKey", do: "clearSelectedIds" },
184
+                "clearBoundsRotation",
183
                 "startBrushSession",
185
                 "startBrushSession",
184
               ],
186
               ],
185
               on: {
187
               on: {
708
     restoreSavedData(data) {
710
     restoreSavedData(data) {
709
       history.load(data)
711
       history.load(data)
710
     },
712
     },
713
+
714
+    clearBoundsRotation(data) {
715
+      data.boundsRotation = 0
716
+    },
711
   },
717
   },
712
   values: {
718
   values: {
713
     selectedIds(data) {
719
     selectedIds(data) {
726
 
732
 
727
       if (selectedIds.size === 0) return null
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
       return getCommonBounds(
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 View File

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

+ 21
- 0
utils/bounds.ts View File

1
 import { Bounds } from "types"
1
 import { Bounds } from "types"
2
+import {
3
+  intersectPolygonBounds,
4
+  intersectPolylineBounds,
5
+} from "./intersections"
2
 
6
 
3
 /**
7
 /**
4
  * Get whether two bounds collide.
8
  * Get whether two bounds collide.
37
   return boundsContain(b, a)
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
  * Get whether two bounds are identical.
62
  * Get whether two bounds are identical.
42
  * @param a Bounds
63
  * @param a Bounds

+ 18
- 0
utils/intersections.ts View File

342
 
342
 
343
   return intersections
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 View File

41
   return bounds
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
 // A helper for getting tangents.
60
 // A helper for getting tangents.
61
 export function getCircleTangentToPoint(
61
 export function getCircleTangentToPoint(
962
   }
962
   }
963
   return point
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
+}

Loading…
Cancel
Save