finishes rotation

This commit is contained in:
Steve Ruiz 2021-05-18 09:32:20 +01:00
parent e2aac4b267
commit 1ece606db0
18 changed files with 353 additions and 198 deletions

View file

@ -6,10 +6,14 @@ import styled from "styles"
export default function BoundsBg() {
const rBounds = useRef<SVGRectElement>(null)
const bounds = useSelector((state) => state.values.selectedBounds)
const singleSelection = useSelector((s) => {
const rotation = useSelector((s) => {
if (s.data.selectedIds.size === 1) {
const { shapes } = s.data.document.pages[s.data.currentPageId]
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)
state.send("POINTED_BOUNDS", inputs.pointerDown(e, "bounds"))
}}
transform={
singleSelection &&
`rotate(${singleSelection.rotation * (180 / Math.PI)},${
minX + width / 2
}, ${minY + width / 2})`
}
transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
minY + height / 2
})`}
/>
)
}

View file

@ -6,15 +6,19 @@ import { TransformCorner, TransformEdge } from "types"
import { lerp } from "utils/utils"
export default function Bounds() {
const zoom = useSelector((state) => state.data.camera.zoom)
const bounds = useSelector((state) => state.values.selectedBounds)
const singleSelection = useSelector((s) => {
const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
const zoom = useSelector((s) => s.data.camera.zoom)
const bounds = useSelector((s) => s.values.selectedBounds)
const rotation = useSelector((s) => {
if (s.data.selectedIds.size === 1) {
const { shapes } = s.data.document.pages[s.data.currentPageId]
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
@ -26,12 +30,9 @@ export default function Bounds() {
return (
<g
pointerEvents={isBrushing ? "none" : "all"}
transform={
singleSelection &&
`rotate(${singleSelection.rotation * (180 / Math.PI)},${
minX + width / 2
}, ${minY + width / 2})`
}
transform={`rotate(${rotation * (180 / Math.PI)},${minX + width / 2}, ${
minY + height / 2
})`}
>
<StyledBounds
x={minX}

View file

@ -5,6 +5,7 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections"
import { pointInCircle } from "utils/hitTests"
import { translateBounds } from "utils/utils"
const circle = createShape<CircleShape>({
boundsCache: new WeakMap([]),
@ -33,27 +34,26 @@ const circle = createShape<CircleShape>({
},
getBounds(shape) {
if (this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
if (!this.boundsCache.has(shape)) {
const { radius } = shape
const bounds = {
minX: 0,
maxX: radius * 2,
minY: 0,
maxY: radius * 2,
width: radius * 2,
height: radius * 2,
}
this.boundsCache.set(shape, bounds)
}
const {
point: [x, y],
radius,
} = shape
return translateBounds(this.boundsCache.get(shape), shape.point)
},
const bounds = {
minX: x,
maxX: x + radius * 2,
minY: y,
maxY: y + radius * 2,
width: radius * 2,
height: radius * 2,
}
this.boundsCache.set(shape, bounds)
return bounds
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getCenter(shape) {

View file

@ -6,6 +6,7 @@ import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections"
import styled from "styles"
import { DotCircle } from "components/canvas/misc"
import { translateBounds } from "utils/utils"
const dot = createShape<DotShape>({
boundsCache: new WeakMap([]),
@ -33,26 +34,24 @@ const dot = createShape<DotShape>({
},
getBounds(shape) {
if (this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
if (!this.boundsCache.has(shape)) {
const bounds = {
minX: 0,
maxX: 1,
minY: 0,
maxY: 1,
width: 1,
height: 1,
}
this.boundsCache.set(shape, bounds)
}
const {
point: [x, y],
} = shape
return translateBounds(this.boundsCache.get(shape), shape.point)
},
const bounds = {
minX: x,
maxX: x + 1,
minY: y,
maxY: y + 1,
width: 1,
height: 1,
}
this.boundsCache.set(shape, bounds)
return bounds
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getCenter(shape) {

View file

@ -5,6 +5,7 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds"
import { intersectEllipseBounds } from "utils/intersections"
import { pointInEllipse } from "utils/hitTests"
import { translateBounds } from "utils/utils"
const ellipse = createShape<EllipseShape>({
boundsCache: new WeakMap([]),
@ -36,28 +37,26 @@ const ellipse = createShape<EllipseShape>({
},
getBounds(shape) {
if (this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
if (!this.boundsCache.has(shape)) {
const { radiusX, radiusY } = shape
const bounds = {
minX: 0,
maxX: radiusX * 2,
minY: 0,
maxY: radiusY * 2,
width: radiusX * 2,
height: radiusY * 2,
}
this.boundsCache.set(shape, bounds)
}
const {
point: [x, y],
radiusX,
radiusY,
} = shape
return translateBounds(this.boundsCache.get(shape), shape.point)
},
const bounds = {
minX: x,
maxX: x + radiusX * 2,
minY: y,
maxY: y + radiusY * 2,
width: radiusX * 2,
height: radiusY * 2,
}
this.boundsCache.set(shape, bounds)
return bounds
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getCenter(shape) {

View file

@ -36,6 +36,9 @@ export interface ShapeUtility<K extends Shape> {
// Get the bounds of the a shape.
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
getCenter(this: ShapeUtility<K>, shape: K): number[]

View file

@ -5,6 +5,7 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections"
import { DotCircle } from "components/canvas/misc"
import { translateBounds } from "utils/utils"
const line = createShape<LineShape>({
boundsCache: new WeakMap([]),
@ -41,26 +42,24 @@ const line = createShape<LineShape>({
},
getBounds(shape) {
if (this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
if (!this.boundsCache.has(shape)) {
const bounds = {
minX: 0,
maxX: 1,
minY: 0,
maxY: 1,
width: 1,
height: 1,
}
this.boundsCache.set(shape, bounds)
}
const {
point: [x, y],
} = shape
return translateBounds(this.boundsCache.get(shape), shape.point)
},
const bounds = {
minX: x,
maxX: x + 1,
minY: y,
maxY: y + 1,
width: 1,
height: 1,
}
this.boundsCache.set(shape, bounds)
return bounds
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getCenter(shape) {

View file

@ -3,7 +3,12 @@ import * as vec from "utils/vec"
import { PolylineShape, ShapeType } from "types"
import { createShape } from "./index"
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>({
boundsCache: new WeakMap([]),
@ -29,33 +34,16 @@ const polyline = createShape<PolylineShape>({
},
getBounds(shape) {
if (this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
if (!this.boundsCache.has(shape)) {
const bounds = getBoundsFromPoints(shape.points)
this.boundsCache.set(shape, bounds)
}
let minX = 0
let minY = 0
let maxX = 0
let maxY = 0
return translateBounds(this.boundsCache.get(shape), shape.point)
},
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)
return bounds
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getCenter(shape) {
@ -78,15 +66,23 @@ const polyline = createShape<PolylineShape>({
return false
},
hitTestBounds(this, shape, bounds) {
const shapeBounds = this.getBounds(shape)
hitTestBounds(this, shape, brushBounds) {
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 (
boundsContained(shapeBounds, bounds) ||
(boundsCollide(shapeBounds, bounds) &&
intersectPolylineBounds(
shape.points.map((point) => vec.add(point, shape.point)),
bounds
).length > 0)
boundsContainPolygon(brushBounds, rotatedCorners) ||
intersectPolylineBounds(
shape.points.map((point) => vec.add(point, shape.point)),
brushBounds
).length > 0
)
},

View file

@ -5,6 +5,7 @@ import { createShape } from "./index"
import { boundsContained } from "utils/bounds"
import { intersectCircleBounds } from "utils/intersections"
import { DotCircle } from "components/canvas/misc"
import { translateBounds } from "utils/utils"
const ray = createShape<RayShape>({
boundsCache: new WeakMap([]),
@ -40,27 +41,25 @@ const ray = createShape<RayShape>({
)
},
getRotatedBounds(shape) {
return this.getBounds(shape)
},
getBounds(shape) {
if (this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
if (!this.boundsCache.has(shape)) {
const bounds = {
minX: 0,
maxX: 1,
minY: 0,
maxY: 1,
width: 1,
height: 1,
}
this.boundsCache.set(shape, bounds)
}
const {
point: [x, y],
} = shape
const bounds = {
minX: x,
maxX: x + 8,
minY: y,
maxY: y + 8,
width: 8,
height: 8,
}
this.boundsCache.set(shape, bounds)
return bounds
return translateBounds(this.boundsCache.get(shape), shape.point)
},
getCenter(shape) {

View file

@ -2,7 +2,8 @@ import { v4 as uuid } from "uuid"
import * as vec from "utils/vec"
import { RectangleShape, ShapeType } from "types"
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>({
boundsCache: new WeakMap([]),
@ -31,31 +32,40 @@ const rectangle = createShape<RectangleShape>({
},
getBounds(shape) {
if (this.boundsCache.has(shape)) {
return this.boundsCache.get(shape)
if (!this.boundsCache.has(shape)) {
const [width, height] = shape.size
const bounds = {
minX: 0,
maxX: width,
minY: 0,
maxY: height,
width,
height,
}
this.boundsCache.set(shape, bounds)
}
const {
point: [x, y],
size: [width, height],
} = shape
return translateBounds(this.boundsCache.get(shape), shape.point)
},
const bounds = {
minX: x,
maxX: x + width,
minY: y,
maxY: y + height,
width,
height,
}
getRotatedBounds(shape) {
const b = this.getBounds(shape)
const center = [b.minX + b.width / 2, b.minY + b.height / 2]
this.boundsCache.set(shape, bounds)
// 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 bounds
return getBoundsFromPoints(rotatedCorners)
},
getCenter(shape) {
const bounds = this.getBounds(shape)
const bounds = this.getRotatedBounds(shape)
return [bounds.minX + bounds.width / 2, bounds.minY + bounds.height / 2]
},
@ -64,10 +74,19 @@ const rectangle = createShape<RectangleShape>({
},
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 (
boundsContained(shapeBounds, brushBounds) ||
boundsCollide(shapeBounds, brushBounds)
boundsContainPolygon(brushBounds, rotatedCorners) ||
boundsCollidePolygon(brushBounds, rotatedCorners)
)
},

View file

@ -16,16 +16,24 @@ export default function translateCommand(
do(data) {
const { shapes } = data.document.pages[after.currentPageId]
for (let { id, rotation } of after.shapes) {
shapes[id].rotation = rotation
for (let { id, point, rotation } of after.shapes) {
const shape = shapes[id]
shape.rotation = rotation
shape.point = point
}
data.boundsRotation = after.boundsRotation
},
undo(data) {
const { shapes } = data.document.pages[before.currentPageId]
for (let { id, rotation } of before.shapes) {
shapes[id].rotation = rotation
for (let { id, point, rotation } of before.shapes) {
const shape = shapes[id]
shape.rotation = rotation
shape.point = point
}
data.boundsRotation = before.boundsRotation
},
})
)

View file

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

View file

@ -18,24 +18,34 @@ export default class RotateSession extends BaseSession {
}
update(data: Data, point: number[]) {
const { currentPageId, center, shapes } = this.snapshot
const { currentPageId, boundsCenter, shapes } = this.snapshot
const { document } = data
const a1 = vec.angle(center, this.origin)
const a2 = vec.angle(center, point)
const a1 = vec.angle(boundsCenter, this.origin)
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]
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) {
const { document } = data
for (let shape of this.snapshot.shapes) {
document.pages[this.snapshot.currentPageId].shapes[shape.id].rotation =
shape.rotation
for (let { id, point, rotation } of this.snapshot.shapes) {
const shape = document.pages[this.snapshot.currentPageId].shapes[id]
shape.rotation = rotation
shape.point = point
}
}
@ -46,9 +56,10 @@ export default class RotateSession extends BaseSession {
export function getRotateSnapshot(data: Data) {
const {
boundsRotation,
selectedIds,
document: { pages },
currentPageId,
document: { pages },
} = current(data)
const shapes = Array.from(selectedIds.values()).map(
@ -63,18 +74,28 @@ export function getRotateSnapshot(data: Data) {
// The common (exterior) bounds of the selected shapes
const bounds = getCommonBounds(...Object.values(shapesBounds))
const center = [
const boundsCenter = [
bounds.minX + bounds.width / 2,
bounds.minY + bounds.height / 2,
]
return {
currentPageId,
center,
shapes: shapes.map(({ id, rotation }) => ({
id,
rotation,
})),
boundsCenter,
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,
point,
rotation,
offset,
center,
}
}),
}
}

View file

@ -29,6 +29,7 @@ const initialData: Data = {
zoom: 1,
},
brush: undefined,
boundsRotation: 0,
pointedId: null,
hoveredId: null,
selectedIds: new Set([]),
@ -180,6 +181,7 @@ const state = createState({
brushSelecting: {
onEnter: [
{ unless: "isPressingShiftKey", do: "clearSelectedIds" },
"clearBoundsRotation",
"startBrushSession",
],
on: {
@ -708,6 +710,10 @@ const state = createState({
restoreSavedData(data) {
history.load(data)
},
clearBoundsRotation(data) {
data.boundsRotation = 0
},
},
values: {
selectedIds(data) {
@ -726,12 +732,14 @@ const state = createState({
if (selectedIds.size === 0) return null
if (selectedIds.size === 1 && !getShapeUtils(shapes[0]).canTransform) {
return null
if (selectedIds.size === 1) {
const shapeUtils = getShapeUtils(shapes[0])
if (!shapeUtils.canTransform) return null
return shapeUtils.getBounds(shapes[0])
}
return getCommonBounds(
...shapes.map((shape) => getShapeUtils(shape).getBounds(shape))
...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
)
},
},

View file

@ -18,6 +18,7 @@ export interface Data {
zoom: number
}
brush?: Bounds
boundsRotation: number
selectedIds: Set<string>
pointedId?: string
hoveredId?: string
@ -168,6 +169,10 @@ export interface Bounds {
height: number
}
export interface RotatedBounds extends Bounds {
rotation: number
}
export interface ShapeBounds extends Bounds {
id: string
}

View file

@ -1,4 +1,8 @@
import { Bounds } from "types"
import {
intersectPolygonBounds,
intersectPolylineBounds,
} from "./intersections"
/**
* Get whether two bounds collide.
@ -37,6 +41,23 @@ export function boundsContained(a: Bounds, b: Bounds) {
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.
* @param a Bounds

View file

@ -342,3 +342,21 @@ export function intersectPolylineBounds(points: number[][], bounds: Bounds) {
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
}

View file

@ -41,21 +41,21 @@ export function getCommonBounds(...b: Bounds[]) {
return bounds
}
export function getBoundsFromPoints(a: number[], b: number[]) {
const minX = Math.min(a[0], b[0])
const maxX = Math.max(a[0], b[0])
const minY = Math.min(a[1], b[1])
const maxY = Math.max(a[1], b[1])
// export function getBoundsFromPoints(a: number[], b: number[]) {
// const minX = Math.min(a[0], b[0])
// const maxX = Math.max(a[0], b[0])
// const minY = Math.min(a[1], b[1])
// const maxY = Math.max(a[1], b[1])
return {
minX,
maxX,
minY,
maxY,
width: maxX - minX,
height: maxY - minY,
}
}
// return {
// minX,
// maxX,
// minY,
// maxY,
// width: maxX - minX,
// height: maxY - minY,
// }
// }
// A helper for getting tangents.
export function getCircleTangentToPoint(
@ -962,3 +962,61 @@ export function vectorToPoint(point: number[] | Vector | undefined) {
}
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,
}
}