finishes rotation
This commit is contained in:
parent
e2aac4b267
commit
1ece606db0
18 changed files with 353 additions and 198 deletions
|
@ -6,10 +6,14 @@ import styled from "styles"
|
||||||
export default function BoundsBg() {
|
export default function BoundsBg() {
|
||||||
const rBounds = useRef<SVGRectElement>(null)
|
const rBounds = useRef<SVGRectElement>(null)
|
||||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||||
const singleSelection = useSelector((s) => {
|
|
||||||
|
const rotation = useSelector((s) => {
|
||||||
if (s.data.selectedIds.size === 1) {
|
if (s.data.selectedIds.size === 1) {
|
||||||
|
const { shapes } = s.data.document.pages[s.data.currentPageId]
|
||||||
const selected = Array.from(s.data.selectedIds.values())[0]
|
const selected = Array.from(s.data.selectedIds.values())[0]
|
||||||
return s.data.document.pages[s.data.currentPageId].shapes[selected]
|
return shapes[selected].rotation
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -30,12 +34,9 @@ export default function BoundsBg() {
|
||||||
rBounds.current.setPointerCapture(e.pointerId)
|
rBounds.current.setPointerCapture(e.pointerId)
|
||||||
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
|
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
|
||||||
}}
|
}}
|
||||||
transform={
|
transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
|
||||||
singleSelection &&
|
minY + height / 2
|
||||||
`rotate(${singleSelection.rotation * (180 / Math.PI)},${
|
})`}
|
||||||
minX + width / 2
|
|
||||||
}, ${minY + width / 2})`
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,19 @@ import { TransformCorner, TransformEdge } from "types"
|
||||||
import { lerp } from "utils/utils"
|
import { lerp } from "utils/utils"
|
||||||
|
|
||||||
export default function Bounds() {
|
export default function Bounds() {
|
||||||
const zoom = useSelector((state) => state.data.camera.zoom)
|
const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
|
||||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
const zoom = useSelector((s) => s.data.camera.zoom)
|
||||||
const singleSelection = useSelector((s) => {
|
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||||
|
|
||||||
|
const rotation = useSelector((s) => {
|
||||||
if (s.data.selectedIds.size === 1) {
|
if (s.data.selectedIds.size === 1) {
|
||||||
|
const { shapes } = s.data.document.pages[s.data.currentPageId]
|
||||||
const selected = Array.from(s.data.selectedIds.values())[0]
|
const selected = Array.from(s.data.selectedIds.values())[0]
|
||||||
return s.data.document.pages[s.data.currentPageId].shapes[selected]
|
return shapes[selected].rotation
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
|
|
||||||
|
|
||||||
if (!bounds) return null
|
if (!bounds) return null
|
||||||
|
|
||||||
|
@ -26,12 +30,9 @@ export default function Bounds() {
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
pointerEvents={isBrushing ? "none" : "all"}
|
pointerEvents={isBrushing ? "none" : "all"}
|
||||||
transform={
|
transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
|
||||||
singleSelection &&
|
minY + height / 2
|
||||||
`rotate(${singleSelection.rotation * (180 / Math.PI)},${
|
})`}
|
||||||
minX + width / 2
|
|
||||||
}, ${minY + width / 2})`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<StyledBounds
|
<StyledBounds
|
||||||
x={minX}
|
x={minX}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { createShape } from "./index"
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from "utils/bounds"
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from "utils/intersections"
|
||||||
import { pointInCircle } from "utils/hitTests"
|
import { pointInCircle } from "utils/hitTests"
|
||||||
|
import { translateBounds } from "utils/utils"
|
||||||
|
|
||||||
const circle = createShape<CircleShape>({
|
const circle = createShape<CircleShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -33,27 +34,26 @@ const circle = createShape<CircleShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
if (this.boundsCache.has(shape)) {
|
if (!this.boundsCache.has(shape)) {
|
||||||
return this.boundsCache.get(shape)
|
const { radius } = shape
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
point: [x, y],
|
|
||||||
radius,
|
|
||||||
} = shape
|
|
||||||
|
|
||||||
const bounds = {
|
const bounds = {
|
||||||
minX: x,
|
minX: 0,
|
||||||
maxX: x + radius * 2,
|
maxX: radius * 2,
|
||||||
minY: y,
|
minY: 0,
|
||||||
maxY: y + radius * 2,
|
maxY: radius * 2,
|
||||||
width: radius * 2,
|
width: radius * 2,
|
||||||
height: radius * 2,
|
height: radius * 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.boundsCache.set(shape, bounds)
|
this.boundsCache.set(shape, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
return bounds
|
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||||
|
},
|
||||||
|
|
||||||
|
getRotatedBounds(shape) {
|
||||||
|
return this.getBounds(shape)
|
||||||
},
|
},
|
||||||
|
|
||||||
getCenter(shape) {
|
getCenter(shape) {
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { boundsContained } from "utils/bounds"
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from "utils/intersections"
|
||||||
import styled from "styles"
|
import styled from "styles"
|
||||||
import { DotCircle } from "components/canvas/misc"
|
import { DotCircle } from "components/canvas/misc"
|
||||||
|
import { translateBounds } from "utils/utils"
|
||||||
|
|
||||||
const dot = createShape<DotShape>({
|
const dot = createShape<DotShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -33,26 +34,24 @@ const dot = createShape<DotShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
if (this.boundsCache.has(shape)) {
|
if (!this.boundsCache.has(shape)) {
|
||||||
return this.boundsCache.get(shape)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
point: [x, y],
|
|
||||||
} = shape
|
|
||||||
|
|
||||||
const bounds = {
|
const bounds = {
|
||||||
minX: x,
|
minX: 0,
|
||||||
maxX: x + 1,
|
maxX: 1,
|
||||||
minY: y,
|
minY: 0,
|
||||||
maxY: y + 1,
|
maxY: 1,
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.boundsCache.set(shape, bounds)
|
this.boundsCache.set(shape, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
return bounds
|
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||||
|
},
|
||||||
|
|
||||||
|
getRotatedBounds(shape) {
|
||||||
|
return this.getBounds(shape)
|
||||||
},
|
},
|
||||||
|
|
||||||
getCenter(shape) {
|
getCenter(shape) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { createShape } from "./index"
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from "utils/bounds"
|
||||||
import { intersectEllipseBounds } from "utils/intersections"
|
import { intersectEllipseBounds } from "utils/intersections"
|
||||||
import { pointInEllipse } from "utils/hitTests"
|
import { pointInEllipse } from "utils/hitTests"
|
||||||
|
import { translateBounds } from "utils/utils"
|
||||||
|
|
||||||
const ellipse = createShape<EllipseShape>({
|
const ellipse = createShape<EllipseShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -36,28 +37,26 @@ const ellipse = createShape<EllipseShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
if (this.boundsCache.has(shape)) {
|
if (!this.boundsCache.has(shape)) {
|
||||||
return this.boundsCache.get(shape)
|
const { radiusX, radiusY } = shape
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
point: [x, y],
|
|
||||||
radiusX,
|
|
||||||
radiusY,
|
|
||||||
} = shape
|
|
||||||
|
|
||||||
const bounds = {
|
const bounds = {
|
||||||
minX: x,
|
minX: 0,
|
||||||
maxX: x + radiusX * 2,
|
maxX: radiusX * 2,
|
||||||
minY: y,
|
minY: 0,
|
||||||
maxY: y + radiusY * 2,
|
maxY: radiusY * 2,
|
||||||
width: radiusX * 2,
|
width: radiusX * 2,
|
||||||
height: radiusY * 2,
|
height: radiusY * 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.boundsCache.set(shape, bounds)
|
this.boundsCache.set(shape, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
return bounds
|
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||||
|
},
|
||||||
|
|
||||||
|
getRotatedBounds(shape) {
|
||||||
|
return this.getBounds(shape)
|
||||||
},
|
},
|
||||||
|
|
||||||
getCenter(shape) {
|
getCenter(shape) {
|
||||||
|
|
|
@ -36,6 +36,9 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
// Get the bounds of the a shape.
|
// Get the bounds of the a shape.
|
||||||
getBounds(this: ShapeUtility<K>, shape: K): Bounds
|
getBounds(this: ShapeUtility<K>, shape: K): Bounds
|
||||||
|
|
||||||
|
// Get the routated bounds of the a shape.
|
||||||
|
getRotatedBounds(this: ShapeUtility<K>, shape: K): Bounds
|
||||||
|
|
||||||
// Get the center of the shape
|
// Get the center of the shape
|
||||||
getCenter(this: ShapeUtility<K>, shape: K): number[]
|
getCenter(this: ShapeUtility<K>, shape: K): number[]
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { createShape } from "./index"
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from "utils/bounds"
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from "utils/intersections"
|
||||||
import { DotCircle } from "components/canvas/misc"
|
import { DotCircle } from "components/canvas/misc"
|
||||||
|
import { translateBounds } from "utils/utils"
|
||||||
|
|
||||||
const line = createShape<LineShape>({
|
const line = createShape<LineShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -41,26 +42,24 @@ const line = createShape<LineShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
if (this.boundsCache.has(shape)) {
|
if (!this.boundsCache.has(shape)) {
|
||||||
return this.boundsCache.get(shape)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
point: [x, y],
|
|
||||||
} = shape
|
|
||||||
|
|
||||||
const bounds = {
|
const bounds = {
|
||||||
minX: x,
|
minX: 0,
|
||||||
maxX: x + 1,
|
maxX: 1,
|
||||||
minY: y,
|
minY: 0,
|
||||||
maxY: y + 1,
|
maxY: 1,
|
||||||
width: 1,
|
width: 1,
|
||||||
height: 1,
|
height: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.boundsCache.set(shape, bounds)
|
this.boundsCache.set(shape, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
return bounds
|
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||||
|
},
|
||||||
|
|
||||||
|
getRotatedBounds(shape) {
|
||||||
|
return this.getBounds(shape)
|
||||||
},
|
},
|
||||||
|
|
||||||
getCenter(shape) {
|
getCenter(shape) {
|
||||||
|
|
|
@ -3,7 +3,12 @@ import * as vec from "utils/vec"
|
||||||
import { PolylineShape, ShapeType } from "types"
|
import { PolylineShape, ShapeType } from "types"
|
||||||
import { createShape } from "./index"
|
import { createShape } from "./index"
|
||||||
import { intersectPolylineBounds } from "utils/intersections"
|
import { intersectPolylineBounds } from "utils/intersections"
|
||||||
import { boundsCollide, boundsContained } from "utils/bounds"
|
import {
|
||||||
|
boundsCollide,
|
||||||
|
boundsContained,
|
||||||
|
boundsContainPolygon,
|
||||||
|
} from "utils/bounds"
|
||||||
|
import { getBoundsFromPoints, translateBounds } from "utils/utils"
|
||||||
|
|
||||||
const polyline = createShape<PolylineShape>({
|
const polyline = createShape<PolylineShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -29,33 +34,16 @@ const polyline = createShape<PolylineShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
if (this.boundsCache.has(shape)) {
|
if (!this.boundsCache.has(shape)) {
|
||||||
return this.boundsCache.get(shape)
|
const bounds = getBoundsFromPoints(shape.points)
|
||||||
}
|
|
||||||
|
|
||||||
let minX = 0
|
|
||||||
let minY = 0
|
|
||||||
let maxX = 0
|
|
||||||
let maxY = 0
|
|
||||||
|
|
||||||
for (let [x, y] of shape.points) {
|
|
||||||
minX = Math.min(x, minX)
|
|
||||||
minY = Math.min(y, minY)
|
|
||||||
maxX = Math.max(x, maxX)
|
|
||||||
maxY = Math.max(y, maxY)
|
|
||||||
}
|
|
||||||
|
|
||||||
const bounds = {
|
|
||||||
minX: minX + shape.point[0],
|
|
||||||
minY: minY + shape.point[1],
|
|
||||||
maxX: maxX + shape.point[0],
|
|
||||||
maxY: maxY + shape.point[1],
|
|
||||||
width: maxX - minX,
|
|
||||||
height: maxY - minY,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.boundsCache.set(shape, bounds)
|
this.boundsCache.set(shape, bounds)
|
||||||
return bounds
|
}
|
||||||
|
|
||||||
|
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||||
|
},
|
||||||
|
|
||||||
|
getRotatedBounds(shape) {
|
||||||
|
return this.getBounds(shape)
|
||||||
},
|
},
|
||||||
|
|
||||||
getCenter(shape) {
|
getCenter(shape) {
|
||||||
|
@ -78,15 +66,23 @@ const polyline = createShape<PolylineShape>({
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTestBounds(this, shape, bounds) {
|
hitTestBounds(this, shape, brushBounds) {
|
||||||
const shapeBounds = this.getBounds(shape)
|
const b = this.getBounds(shape)
|
||||||
|
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
|
||||||
|
|
||||||
|
const rotatedCorners = [
|
||||||
|
[b.minX, b.minY],
|
||||||
|
[b.maxX, b.minY],
|
||||||
|
[b.maxX, b.maxY],
|
||||||
|
[b.minX, b.maxY],
|
||||||
|
].map((point) => vec.rotWith(point, center, shape.rotation))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
boundsContained(shapeBounds, bounds) ||
|
boundsContainPolygon(brushBounds, rotatedCorners) ||
|
||||||
(boundsCollide(shapeBounds, bounds) &&
|
|
||||||
intersectPolylineBounds(
|
intersectPolylineBounds(
|
||||||
shape.points.map((point) => vec.add(point, shape.point)),
|
shape.points.map((point) => vec.add(point, shape.point)),
|
||||||
bounds
|
brushBounds
|
||||||
).length > 0)
|
).length > 0
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { createShape } from "./index"
|
||||||
import { boundsContained } from "utils/bounds"
|
import { boundsContained } from "utils/bounds"
|
||||||
import { intersectCircleBounds } from "utils/intersections"
|
import { intersectCircleBounds } from "utils/intersections"
|
||||||
import { DotCircle } from "components/canvas/misc"
|
import { DotCircle } from "components/canvas/misc"
|
||||||
|
import { translateBounds } from "utils/utils"
|
||||||
|
|
||||||
const ray = createShape<RayShape>({
|
const ray = createShape<RayShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -40,27 +41,25 @@ const ray = createShape<RayShape>({
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getRotatedBounds(shape) {
|
||||||
|
return this.getBounds(shape)
|
||||||
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
if (this.boundsCache.has(shape)) {
|
if (!this.boundsCache.has(shape)) {
|
||||||
return this.boundsCache.get(shape)
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
point: [x, y],
|
|
||||||
} = shape
|
|
||||||
|
|
||||||
const bounds = {
|
const bounds = {
|
||||||
minX: x,
|
minX: 0,
|
||||||
maxX: x + 8,
|
maxX: 1,
|
||||||
minY: y,
|
minY: 0,
|
||||||
maxY: y + 8,
|
maxY: 1,
|
||||||
width: 8,
|
width: 1,
|
||||||
height: 8,
|
height: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.boundsCache.set(shape, bounds)
|
this.boundsCache.set(shape, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
return bounds
|
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||||
},
|
},
|
||||||
|
|
||||||
getCenter(shape) {
|
getCenter(shape) {
|
||||||
|
|
|
@ -2,7 +2,8 @@ import { v4 as uuid } from "uuid"
|
||||||
import * as vec from "utils/vec"
|
import * as vec from "utils/vec"
|
||||||
import { RectangleShape, ShapeType } from "types"
|
import { RectangleShape, ShapeType } from "types"
|
||||||
import { createShape } from "./index"
|
import { createShape } from "./index"
|
||||||
import { boundsContained, boundsCollide } from "utils/bounds"
|
import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds"
|
||||||
|
import { getBoundsFromPoints, rotateBounds, translateBounds } from "utils/utils"
|
||||||
|
|
||||||
const rectangle = createShape<RectangleShape>({
|
const rectangle = createShape<RectangleShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -31,31 +32,40 @@ const rectangle = createShape<RectangleShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
if (this.boundsCache.has(shape)) {
|
if (!this.boundsCache.has(shape)) {
|
||||||
return this.boundsCache.get(shape)
|
const [width, height] = shape.size
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
point: [x, y],
|
|
||||||
size: [width, height],
|
|
||||||
} = shape
|
|
||||||
|
|
||||||
const bounds = {
|
const bounds = {
|
||||||
minX: x,
|
minX: 0,
|
||||||
maxX: x + width,
|
maxX: width,
|
||||||
minY: y,
|
minY: 0,
|
||||||
maxY: y + height,
|
maxY: height,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.boundsCache.set(shape, bounds)
|
this.boundsCache.set(shape, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
return bounds
|
return translateBounds(this.boundsCache.get(shape), shape.point)
|
||||||
|
},
|
||||||
|
|
||||||
|
getRotatedBounds(shape) {
|
||||||
|
const b = this.getBounds(shape)
|
||||||
|
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
|
||||||
|
|
||||||
|
// Rotate corners of the shape, then find the minimum among those points.
|
||||||
|
const rotatedCorners = [
|
||||||
|
[b.minX, b.minY],
|
||||||
|
[b.maxX, b.minY],
|
||||||
|
[b.maxX, b.maxY],
|
||||||
|
[b.minX, b.maxY],
|
||||||
|
].map((point) => vec.rotWith(point, center, shape.rotation))
|
||||||
|
|
||||||
|
return getBoundsFromPoints(rotatedCorners)
|
||||||
},
|
},
|
||||||
|
|
||||||
getCenter(shape) {
|
getCenter(shape) {
|
||||||
const bounds = this.getBounds(shape)
|
const bounds = this.getRotatedBounds(shape)
|
||||||
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
|
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -64,10 +74,19 @@ const rectangle = createShape<RectangleShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTestBounds(shape, brushBounds) {
|
hitTestBounds(shape, brushBounds) {
|
||||||
const shapeBounds = this.getBounds(shape)
|
const b = this.getBounds(shape)
|
||||||
|
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
|
||||||
|
|
||||||
|
const rotatedCorners = [
|
||||||
|
[b.minX, b.minY],
|
||||||
|
[b.maxX, b.minY],
|
||||||
|
[b.maxX, b.maxY],
|
||||||
|
[b.minX, b.maxY],
|
||||||
|
].map((point) => vec.rotWith(point, center, shape.rotation))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
boundsContained(shapeBounds, brushBounds) ||
|
boundsContainPolygon(brushBounds, rotatedCorners) ||
|
||||||
boundsCollide(shapeBounds, brushBounds)
|
boundsCollidePolygon(brushBounds, rotatedCorners)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -16,16 +16,24 @@ export default function translateCommand(
|
||||||
do(data) {
|
do(data) {
|
||||||
const { shapes } = data.document.pages[after.currentPageId]
|
const { shapes } = data.document.pages[after.currentPageId]
|
||||||
|
|
||||||
for (let { id, rotation } of after.shapes) {
|
for (let { id, point, rotation } of after.shapes) {
|
||||||
shapes[id].rotation = rotation
|
const shape = shapes[id]
|
||||||
|
shape.rotation = rotation
|
||||||
|
shape.point = point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.boundsRotation = after.boundsRotation
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { shapes } = data.document.pages[before.currentPageId]
|
const { shapes } = data.document.pages[before.currentPageId]
|
||||||
|
|
||||||
for (let { id, rotation } of before.shapes) {
|
for (let { id, point, rotation } of before.shapes) {
|
||||||
shapes[id].rotation = rotation
|
const shape = shapes[id]
|
||||||
|
shape.rotation = rotation
|
||||||
|
shape.point = point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data.boundsRotation = before.boundsRotation
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default class BrushSession extends BaseSession {
|
||||||
update = (data: Data, point: number[]) => {
|
update = (data: Data, point: number[]) => {
|
||||||
const { origin, snapshot } = this
|
const { origin, snapshot } = this
|
||||||
|
|
||||||
const brushBounds = getBoundsFromPoints(origin, point)
|
const brushBounds = getBoundsFromPoints([origin, point])
|
||||||
|
|
||||||
for (let { test, id } of snapshot.shapes) {
|
for (let { test, id } of snapshot.shapes) {
|
||||||
if (test(brushBounds)) {
|
if (test(brushBounds)) {
|
||||||
|
|
|
@ -18,24 +18,34 @@ export default class RotateSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
update(data: Data, point: number[]) {
|
update(data: Data, point: number[]) {
|
||||||
const { currentPageId, center, shapes } = this.snapshot
|
const { currentPageId, boundsCenter, shapes } = this.snapshot
|
||||||
const { document } = data
|
const { document } = data
|
||||||
|
|
||||||
const a1 = vec.angle(center, this.origin)
|
const a1 = vec.angle(boundsCenter, this.origin)
|
||||||
const a2 = vec.angle(center, point)
|
const a2 = vec.angle(boundsCenter, point)
|
||||||
|
|
||||||
for (let { id, rotation } of shapes) {
|
data.boundsRotation =
|
||||||
|
(this.snapshot.boundsRotation + (a2 - a1)) % (Math.PI * 2)
|
||||||
|
|
||||||
|
for (let { id, center, offset, rotation } of shapes) {
|
||||||
const shape = document.pages[currentPageId].shapes[id]
|
const shape = document.pages[currentPageId].shapes[id]
|
||||||
shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
|
shape.rotation = rotation + ((a2 - a1) % (Math.PI * 2))
|
||||||
|
const newCenter = vec.rotWith(
|
||||||
|
center,
|
||||||
|
boundsCenter,
|
||||||
|
(a2 - a1) % (Math.PI * 2)
|
||||||
|
)
|
||||||
|
shape.point = vec.sub(newCenter, offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data) {
|
cancel(data: Data) {
|
||||||
const { document } = data
|
const { document } = data
|
||||||
|
|
||||||
for (let shape of this.snapshot.shapes) {
|
for (let { id, point, rotation } of this.snapshot.shapes) {
|
||||||
document.pages[this.snapshot.currentPageId].shapes[shape.id].rotation =
|
const shape = document.pages[this.snapshot.currentPageId].shapes[id]
|
||||||
shape.rotation
|
shape.rotation = rotation
|
||||||
|
shape.point = point
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,9 +56,10 @@ export default class RotateSession extends BaseSession {
|
||||||
|
|
||||||
export function getRotateSnapshot(data: Data) {
|
export function getRotateSnapshot(data: Data) {
|
||||||
const {
|
const {
|
||||||
|
boundsRotation,
|
||||||
selectedIds,
|
selectedIds,
|
||||||
document: { pages },
|
|
||||||
currentPageId,
|
currentPageId,
|
||||||
|
document: { pages },
|
||||||
} = current(data)
|
} = current(data)
|
||||||
|
|
||||||
const shapes = Array.from(selectedIds.values()).map(
|
const shapes = Array.from(selectedIds.values()).map(
|
||||||
|
@ -63,18 +74,28 @@ export function getRotateSnapshot(data: Data) {
|
||||||
// The common (exterior) bounds of the selected shapes
|
// The common (exterior) bounds of the selected shapes
|
||||||
const bounds = getCommonBounds(...Object.values(shapesBounds))
|
const bounds = getCommonBounds(...Object.values(shapesBounds))
|
||||||
|
|
||||||
const center = [
|
const boundsCenter = [
|
||||||
bounds.minX + bounds.width / 2,
|
bounds.minX + bounds.width / 2,
|
||||||
bounds.minY + bounds.height / 2,
|
bounds.minY + bounds.height / 2,
|
||||||
]
|
]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentPageId,
|
currentPageId,
|
||||||
center,
|
boundsCenter,
|
||||||
shapes: shapes.map(({ id, rotation }) => ({
|
boundsRotation,
|
||||||
|
shapes: shapes.map(({ id, point, rotation }) => {
|
||||||
|
const bounds = shapesBounds[id]
|
||||||
|
const offset = [bounds.width / 2, bounds.height / 2]
|
||||||
|
const center = vec.add(offset, [bounds.minX, bounds.minY])
|
||||||
|
|
||||||
|
return {
|
||||||
id,
|
id,
|
||||||
|
point,
|
||||||
rotation,
|
rotation,
|
||||||
})),
|
offset,
|
||||||
|
center,
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -29,6 +29,7 @@ const initialData: Data = {
|
||||||
zoom: 1,
|
zoom: 1,
|
||||||
},
|
},
|
||||||
brush: undefined,
|
brush: undefined,
|
||||||
|
boundsRotation: 0,
|
||||||
pointedId: null,
|
pointedId: null,
|
||||||
hoveredId: null,
|
hoveredId: null,
|
||||||
selectedIds: new Set([]),
|
selectedIds: new Set([]),
|
||||||
|
@ -180,6 +181,7 @@ const state = createState({
|
||||||
brushSelecting: {
|
brushSelecting: {
|
||||||
onEnter: [
|
onEnter: [
|
||||||
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
|
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
|
||||||
|
"clearBoundsRotation",
|
||||||
"startBrushSession",
|
"startBrushSession",
|
||||||
],
|
],
|
||||||
on: {
|
on: {
|
||||||
|
@ -708,6 +710,10 @@ const state = createState({
|
||||||
restoreSavedData(data) {
|
restoreSavedData(data) {
|
||||||
history.load(data)
|
history.load(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clearBoundsRotation(data) {
|
||||||
|
data.boundsRotation = 0
|
||||||
|
},
|
||||||
},
|
},
|
||||||
values: {
|
values: {
|
||||||
selectedIds(data) {
|
selectedIds(data) {
|
||||||
|
@ -726,12 +732,14 @@ const state = createState({
|
||||||
|
|
||||||
if (selectedIds.size === 0) return null
|
if (selectedIds.size === 0) return null
|
||||||
|
|
||||||
if (selectedIds.size === 1 && !getShapeUtils(shapes[0]).canTransform) {
|
if (selectedIds.size === 1) {
|
||||||
return null
|
const shapeUtils = getShapeUtils(shapes[0])
|
||||||
|
if (!shapeUtils.canTransform) return null
|
||||||
|
return shapeUtils.getBounds(shapes[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
return getCommonBounds(
|
return getCommonBounds(
|
||||||
...shapes.map((shape) => getShapeUtils(shape).getBounds(shape))
|
...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
5
types.ts
5
types.ts
|
@ -18,6 +18,7 @@ export interface Data {
|
||||||
zoom: number
|
zoom: number
|
||||||
}
|
}
|
||||||
brush?: Bounds
|
brush?: Bounds
|
||||||
|
boundsRotation: number
|
||||||
selectedIds: Set<string>
|
selectedIds: Set<string>
|
||||||
pointedId?: string
|
pointedId?: string
|
||||||
hoveredId?: string
|
hoveredId?: string
|
||||||
|
@ -168,6 +169,10 @@ export interface Bounds {
|
||||||
height: number
|
height: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RotatedBounds extends Bounds {
|
||||||
|
rotation: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface ShapeBounds extends Bounds {
|
export interface ShapeBounds extends Bounds {
|
||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
import { Bounds } from "types"
|
import { Bounds } from "types"
|
||||||
|
import {
|
||||||
|
intersectPolygonBounds,
|
||||||
|
intersectPolylineBounds,
|
||||||
|
} from "./intersections"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whether two bounds collide.
|
* Get whether two bounds collide.
|
||||||
|
@ -37,6 +41,23 @@ export function boundsContained(a: Bounds, b: Bounds) {
|
||||||
return boundsContain(b, a)
|
return boundsContain(b, a)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether a set of points are all contained by a bounding box.
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function boundsContainPolygon(a: Bounds, points: number[][]) {
|
||||||
|
return points.every((point) => pointInBounds(point, a))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get whether a polygon collides a bounding box.
|
||||||
|
* @param points
|
||||||
|
* @param b
|
||||||
|
*/
|
||||||
|
export function boundsCollidePolygon(a: Bounds, points: number[][]) {
|
||||||
|
return intersectPolygonBounds(points, a).length > 0
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get whether two bounds are identical.
|
* Get whether two bounds are identical.
|
||||||
* @param a Bounds
|
* @param a Bounds
|
||||||
|
|
|
@ -342,3 +342,21 @@ export function intersectPolylineBounds(points: number[][], bounds: Bounds) {
|
||||||
|
|
||||||
return intersections
|
return intersections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function intersectPolygonBounds(points: number[][], bounds: Bounds) {
|
||||||
|
const { minX, minY, width, height } = bounds
|
||||||
|
const intersections: Intersection[] = []
|
||||||
|
|
||||||
|
for (let i = 1; i < points.length + 1; i++) {
|
||||||
|
intersections.push(
|
||||||
|
...intersectRectangleLineSegment(
|
||||||
|
[minX, minY],
|
||||||
|
[width, height],
|
||||||
|
points[i - 1],
|
||||||
|
points[i % points.length]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections
|
||||||
|
}
|
||||||
|
|
|
@ -41,21 +41,21 @@ export function getCommonBounds(...b: Bounds[]) {
|
||||||
return bounds
|
return bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBoundsFromPoints(a: number[], b: number[]) {
|
// export function getBoundsFromPoints(a: number[], b: number[]) {
|
||||||
const minX = Math.min(a[0], b[0])
|
// const minX = Math.min(a[0], b[0])
|
||||||
const maxX = Math.max(a[0], b[0])
|
// const maxX = Math.max(a[0], b[0])
|
||||||
const minY = Math.min(a[1], b[1])
|
// const minY = Math.min(a[1], b[1])
|
||||||
const maxY = Math.max(a[1], b[1])
|
// const maxY = Math.max(a[1], b[1])
|
||||||
|
|
||||||
return {
|
// return {
|
||||||
minX,
|
// minX,
|
||||||
maxX,
|
// maxX,
|
||||||
minY,
|
// minY,
|
||||||
maxY,
|
// maxY,
|
||||||
width: maxX - minX,
|
// width: maxX - minX,
|
||||||
height: maxY - minY,
|
// height: maxY - minY,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
// A helper for getting tangents.
|
// A helper for getting tangents.
|
||||||
export function getCircleTangentToPoint(
|
export function getCircleTangentToPoint(
|
||||||
|
@ -962,3 +962,61 @@ export function vectorToPoint(point: number[] | Vector | undefined) {
|
||||||
}
|
}
|
||||||
return point
|
return point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getBoundsFromPoints(points: number[][]): Bounds {
|
||||||
|
let minX = Infinity
|
||||||
|
let minY = Infinity
|
||||||
|
let maxX = -Infinity
|
||||||
|
let maxY = -Infinity
|
||||||
|
|
||||||
|
for (let [x, y] of points) {
|
||||||
|
minX = Math.min(x, minX)
|
||||||
|
minY = Math.min(y, minY)
|
||||||
|
maxX = Math.max(x, maxX)
|
||||||
|
maxY = Math.max(y, maxY)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
maxX,
|
||||||
|
maxY,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a bounding box without recalculating it.
|
||||||
|
* @param bounds
|
||||||
|
* @param delta
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function translateBounds(bounds: Bounds, delta: number[]) {
|
||||||
|
return {
|
||||||
|
minX: bounds.minX + delta[0],
|
||||||
|
minY: bounds.minY + delta[1],
|
||||||
|
maxX: bounds.maxX + delta[0],
|
||||||
|
maxY: bounds.maxY + delta[1],
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rotateBounds(
|
||||||
|
bounds: Bounds,
|
||||||
|
center: number[],
|
||||||
|
rotation: number
|
||||||
|
) {
|
||||||
|
const [minX, minY] = vec.rotWith([bounds.minX, bounds.minY], center, rotation)
|
||||||
|
const [maxX, maxY] = vec.rotWith([bounds.maxX, bounds.maxY], center, rotation)
|
||||||
|
|
||||||
|
return {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
maxX,
|
||||||
|
maxY,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue