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() {
|
||||
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
|
||||
})`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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[]
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
},
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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))
|
||||
)
|
||||
},
|
||||
},
|
||||
|
|
5
types.ts
5
types.ts
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue