Adds bounds
This commit is contained in:
parent
3d52d9e9d2
commit
89d9ddcb1d
14 changed files with 531 additions and 41 deletions
|
@ -1 +1,33 @@
|
|||
export default function BoundsBg() {}
|
||||
import state, { useSelector } from "state"
|
||||
import styled from "styles"
|
||||
|
||||
export default function BoundsBg() {
|
||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||
|
||||
if (!bounds) return null
|
||||
|
||||
const { minX, minY, width, height } = bounds
|
||||
|
||||
return (
|
||||
<StyledBoundsBg
|
||||
x={minX}
|
||||
y={minY}
|
||||
width={width}
|
||||
height={height}
|
||||
onPointerDown={(e) => {
|
||||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS", {
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey || e.ctrlKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledBoundsBg = styled("rect", {
|
||||
fill: "$boundsBg",
|
||||
})
|
||||
|
|
|
@ -1 +1,297 @@
|
|||
export default function Bounds() {}
|
||||
import state, { useSelector } from "state"
|
||||
import { motion } from "framer-motion"
|
||||
import styled from "styles"
|
||||
|
||||
export default function Bounds() {
|
||||
const bounds = useSelector((state) => state.values.selectedBounds)
|
||||
const isBrushing = useSelector((state) => state.isIn("brushSelecting"))
|
||||
const zoom = useSelector((state) => state.data.camera.zoom)
|
||||
|
||||
if (!bounds) return null
|
||||
|
||||
const { minX, minY, maxX, maxY, width, height } = bounds
|
||||
|
||||
const p = 4 / zoom
|
||||
const cp = p * 2
|
||||
|
||||
return (
|
||||
<g pointerEvents={isBrushing ? "none" : "all"}>
|
||||
<StyledBounds
|
||||
x={minX}
|
||||
y={minY}
|
||||
width={width}
|
||||
height={height}
|
||||
pointerEvents="none"
|
||||
/>
|
||||
<Corner
|
||||
x={minX}
|
||||
y={minY}
|
||||
corner={0}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nwse-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={maxX}
|
||||
y={minY}
|
||||
corner={1}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nesw-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={maxX}
|
||||
y={maxY}
|
||||
corner={2}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nwse-resize"
|
||||
/>
|
||||
<Corner
|
||||
x={minX}
|
||||
y={maxY}
|
||||
corner={3}
|
||||
width={cp}
|
||||
height={cp}
|
||||
cursor="nesw-resize"
|
||||
/>
|
||||
<EdgeHorizontal
|
||||
x={minX + p}
|
||||
y={minY}
|
||||
width={Math.max(0, width - p * 2)}
|
||||
height={p}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_EDGE", {
|
||||
edge: 0,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
})
|
||||
document.body.style.cursor = "ns-resize"
|
||||
}}
|
||||
/>
|
||||
<EdgeVertical
|
||||
x={maxX}
|
||||
y={minY + p}
|
||||
width={p}
|
||||
height={Math.max(0, height - p * 2)}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_EDGE", {
|
||||
edge: 1,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
})
|
||||
document.body.style.cursor = "ew-resize"
|
||||
}}
|
||||
/>
|
||||
<EdgeHorizontal
|
||||
x={minX + p}
|
||||
y={maxY}
|
||||
width={Math.max(0, width - p * 2)}
|
||||
height={p}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_EDGE", {
|
||||
edge: 2,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
})
|
||||
document.body.style.cursor = "ns-resize"
|
||||
}}
|
||||
/>
|
||||
<EdgeVertical
|
||||
x={minX}
|
||||
y={minY + p}
|
||||
width={p}
|
||||
height={Math.max(0, height - p * 2)}
|
||||
onSelect={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_EDGE", {
|
||||
edge: 3,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
})
|
||||
document.body.style.cursor = "ew-resize"
|
||||
}}
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
function Corner({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
cursor,
|
||||
onHover,
|
||||
corner,
|
||||
}: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
cursor: string
|
||||
corner: number
|
||||
onHover?: () => void
|
||||
}) {
|
||||
const isTop = corner === 0 || corner === 1
|
||||
const isLeft = corner === 0 || corner === 3
|
||||
return (
|
||||
<g>
|
||||
<motion.rect
|
||||
x={x + width * (isLeft ? -1.25 : -0.5)} // + width * 2 * transformOffset[0]}
|
||||
y={y + width * (isTop ? -1.25 : -0.5)} // + height * 2 * transformOffset[1]}
|
||||
width={width * 1.75}
|
||||
height={height * 1.75}
|
||||
onPanEnd={restoreCursor}
|
||||
onTap={restoreCursor}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_ROTATE_CORNER", {
|
||||
corner,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
})
|
||||
document.body.style.cursor = "grabbing"
|
||||
}}
|
||||
style={{ cursor: "grab" }}
|
||||
fill="transparent"
|
||||
/>
|
||||
<StyledCorner
|
||||
x={x + width * -0.5}
|
||||
y={y + height * -0.5}
|
||||
width={width}
|
||||
height={height}
|
||||
onPointerEnter={onHover}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation()
|
||||
if (e.buttons !== 1) return
|
||||
state.send("POINTED_BOUNDS_CORNER", {
|
||||
corner,
|
||||
shiftKey: e.shiftKey,
|
||||
optionKey: e.altKey,
|
||||
metaKey: e.metaKey,
|
||||
ctrlKey: e.ctrlKey,
|
||||
buttons: e.buttons,
|
||||
})
|
||||
document.body.style.cursor = "nesw-resize"
|
||||
}}
|
||||
onPanEnd={restoreCursor}
|
||||
onTap={restoreCursor}
|
||||
style={{ cursor }}
|
||||
className="strokewidth-ui stroke-bounds fill-corner"
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
}
|
||||
|
||||
function EdgeHorizontal({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
onHover,
|
||||
onSelect,
|
||||
}: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
onHover?: () => void
|
||||
onSelect?: (e: React.PointerEvent) => void
|
||||
}) {
|
||||
return (
|
||||
<StyledEdge
|
||||
x={x}
|
||||
y={y - height / 2}
|
||||
width={width}
|
||||
height={height}
|
||||
onPointerEnter={onHover}
|
||||
onPointerDown={onSelect}
|
||||
onPanEnd={restoreCursor}
|
||||
onTap={restoreCursor}
|
||||
style={{ cursor: "ns-resize" }}
|
||||
direction="horizontal"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function EdgeVertical({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
onHover,
|
||||
onSelect,
|
||||
}: {
|
||||
x: number
|
||||
y: number
|
||||
width: number
|
||||
height: number
|
||||
onHover?: () => void
|
||||
onSelect?: (e: React.PointerEvent) => void
|
||||
}) {
|
||||
return (
|
||||
<StyledEdge
|
||||
x={x - width / 2}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
onPointerEnter={onHover}
|
||||
onPointerDown={onSelect}
|
||||
onPanEnd={restoreCursor}
|
||||
onTap={restoreCursor}
|
||||
direction="vertical"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function restoreCursor() {
|
||||
document.body.style.cursor = "default"
|
||||
state.send("STOPPED_POINTING")
|
||||
}
|
||||
|
||||
const StyledEdge = styled(motion.rect, {
|
||||
stroke: "none",
|
||||
fill: "none",
|
||||
variant: {
|
||||
direction: {
|
||||
horizontal: { cursor: "ns-resize" },
|
||||
vertical: { cursor: "ew-resize" },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledCorner = styled(motion.rect, {
|
||||
stroke: "$bounds",
|
||||
fill: "#fff",
|
||||
zStrokeWidth: 2,
|
||||
})
|
||||
|
||||
const StyledBounds = styled("rect", {
|
||||
fill: "none",
|
||||
stroke: "$bounds",
|
||||
zStrokeWidth: 2,
|
||||
})
|
||||
|
|
|
@ -6,6 +6,8 @@ import useCamera from "hooks/useCamera"
|
|||
import Page from "./page"
|
||||
import Brush from "./brush"
|
||||
import state from "state"
|
||||
import Bounds from "./bounds"
|
||||
import BoundsBg from "./bounds-bg"
|
||||
|
||||
export default function Canvas() {
|
||||
const rCanvas = useRef<SVGSVGElement>(null)
|
||||
|
@ -37,7 +39,9 @@ export default function Canvas() {
|
|||
onPointerUp={handlePointerUp}
|
||||
>
|
||||
<MainGroup ref={rGroup}>
|
||||
<BoundsBg />
|
||||
<Page />
|
||||
<Bounds />
|
||||
<Brush />
|
||||
</MainGroup>
|
||||
</MainSVG>
|
||||
|
|
|
@ -1,19 +1,8 @@
|
|||
import React, { useCallback, useRef } from "react"
|
||||
import React, { useCallback, useRef, memo } from "react"
|
||||
import state, { useSelector } from "state"
|
||||
import styled from "styles"
|
||||
import { getPointerEventInfo } from "utils/utils"
|
||||
import { memo } from "react"
|
||||
import Shapes from "lib/shapes"
|
||||
|
||||
/*
|
||||
Gets the shape from the current page's shapes, using the
|
||||
provided ID. Depending on the shape's type, return the
|
||||
component for that type.
|
||||
|
||||
This component takes an SVG shape as its children. It handles
|
||||
events for the shape as well as provides indicators for hover
|
||||
and selected status
|
||||
*/
|
||||
import shapes from "lib/shapes"
|
||||
import styled from "styles"
|
||||
|
||||
function Shape({ id }: { id: string }) {
|
||||
const rGroup = useRef<SVGGElement>(null)
|
||||
|
@ -66,7 +55,7 @@ function Shape({ id }: { id: string }) {
|
|||
onPointerLeave={handlePointerLeave}
|
||||
>
|
||||
<defs>
|
||||
{Shapes[shape.type] ? Shapes[shape.type].render(shape) : null}
|
||||
{shapes[shape.type] ? shapes[shape.type].render(shape) : null}
|
||||
</defs>
|
||||
<HoverIndicator as="use" xlinkHref={"#" + id} />
|
||||
<use xlinkHref={"#" + id} {...shape.style} />
|
||||
|
@ -78,7 +67,7 @@ function Shape({ id }: { id: string }) {
|
|||
const Indicator = styled("path", {
|
||||
fill: "none",
|
||||
stroke: "transparent",
|
||||
strokeWidth: "max(1, calc(2 / var(--camera-zoom)))",
|
||||
zStrokeWidth: 1,
|
||||
pointerEvents: "none",
|
||||
strokeLineCap: "round",
|
||||
strokeLinejoin: "round",
|
||||
|
@ -87,7 +76,7 @@ const Indicator = styled("path", {
|
|||
const HoverIndicator = styled("path", {
|
||||
fill: "none",
|
||||
stroke: "transparent",
|
||||
strokeWidth: "max(1, calc(8 / var(--camera-zoom)))",
|
||||
zStrokeWidth: 8,
|
||||
pointerEvents: "all",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { BaseLibShape, CircleShape, ShapeType } from "types"
|
||||
import { boundsCache } from "./index"
|
||||
|
||||
const Circle: BaseLibShape<ShapeType.Circle> = {
|
||||
create(props): CircleShape {
|
||||
|
@ -23,19 +24,26 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
|
|||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (boundsCache.has(shape)) {
|
||||
return boundsCache.get(shape)
|
||||
}
|
||||
|
||||
const {
|
||||
point: [cx, cy],
|
||||
point: [x, y],
|
||||
radius,
|
||||
} = shape
|
||||
|
||||
return {
|
||||
minX: cx,
|
||||
maxX: cx + radius * 2,
|
||||
minY: cy,
|
||||
maxY: cy + radius * 2,
|
||||
const bounds = {
|
||||
minX: x,
|
||||
maxX: x + radius * 2,
|
||||
minY: y,
|
||||
maxY: y + radius * 2,
|
||||
width: radius * 2,
|
||||
height: radius * 2,
|
||||
}
|
||||
|
||||
boundsCache.set(shape, bounds)
|
||||
return bounds
|
||||
},
|
||||
|
||||
hitTest(shape, test) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { BaseLibShape, DotShape, ShapeType } from "types"
|
||||
import { boundsCache } from "./index"
|
||||
|
||||
const Dot: BaseLibShape<ShapeType.Dot> = {
|
||||
create(props): DotShape {
|
||||
|
@ -22,18 +23,25 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
|
|||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (boundsCache.has(shape)) {
|
||||
return boundsCache.get(shape)
|
||||
}
|
||||
|
||||
const {
|
||||
point: [cx, cy],
|
||||
point: [x, y],
|
||||
} = shape
|
||||
|
||||
return {
|
||||
minX: cx,
|
||||
maxX: cx + 4,
|
||||
minY: cy,
|
||||
maxY: cy + 4,
|
||||
width: 4,
|
||||
height: 4,
|
||||
const bounds = {
|
||||
minX: x,
|
||||
maxX: x + 8,
|
||||
minY: y,
|
||||
maxY: y + 8,
|
||||
width: 8,
|
||||
height: 8,
|
||||
}
|
||||
|
||||
boundsCache.set(shape, bounds)
|
||||
return bounds
|
||||
},
|
||||
|
||||
hitTest(shape, test) {
|
||||
|
|
|
@ -3,11 +3,15 @@ import Dot from "./dot"
|
|||
import Polyline from "./polyline"
|
||||
import Rectangle from "./rectangle"
|
||||
|
||||
import { ShapeType } from "types"
|
||||
import { Bounds, Shape, ShapeType } from "types"
|
||||
|
||||
export default {
|
||||
export const boundsCache = new WeakMap<Shape, Bounds>([])
|
||||
|
||||
const shapes = {
|
||||
[ShapeType.Circle]: Circle,
|
||||
[ShapeType.Dot]: Dot,
|
||||
[ShapeType.Polyline]: Polyline,
|
||||
[ShapeType.Rectangle]: Rectangle,
|
||||
}
|
||||
|
||||
export default shapes
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { BaseLibShape, PolylineShape, ShapeType } from "types"
|
||||
import { boundsCache } from "./index"
|
||||
|
||||
const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
||||
create(props): PolylineShape {
|
||||
|
@ -23,6 +24,10 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
|||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (boundsCache.has(shape)) {
|
||||
return boundsCache.get(shape)
|
||||
}
|
||||
|
||||
let minX = 0
|
||||
let minY = 0
|
||||
let maxX = 0
|
||||
|
@ -35,7 +40,7 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
|||
maxY = Math.max(y, maxY)
|
||||
}
|
||||
|
||||
return {
|
||||
const bounds = {
|
||||
minX: minX + shape.point[0],
|
||||
minY: minY + shape.point[1],
|
||||
maxX: maxX + shape.point[0],
|
||||
|
@ -43,6 +48,9 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
|||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
}
|
||||
|
||||
boundsCache.set(shape, bounds)
|
||||
return bounds
|
||||
},
|
||||
|
||||
hitTest(shape) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { v4 as uuid } from "uuid"
|
||||
import * as vec from "utils/vec"
|
||||
import { BaseLibShape, RectangleShape, ShapeType } from "types"
|
||||
import { boundsCache } from "./index"
|
||||
|
||||
const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
||||
create(props): RectangleShape {
|
||||
|
@ -23,12 +24,16 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
|||
},
|
||||
|
||||
getBounds(shape) {
|
||||
if (boundsCache.has(shape)) {
|
||||
return boundsCache.get(shape)
|
||||
}
|
||||
|
||||
const {
|
||||
point: [x, y],
|
||||
size: [width, height],
|
||||
} = shape
|
||||
|
||||
return {
|
||||
const bounds = {
|
||||
minX: x,
|
||||
maxX: x + width,
|
||||
minY: y,
|
||||
|
@ -36,6 +41,9 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
|||
width,
|
||||
height,
|
||||
}
|
||||
|
||||
boundsCache.set(shape, bounds)
|
||||
return bounds
|
||||
},
|
||||
|
||||
hitTest(shape) {
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
"@state-designer/react": "^1.7.1",
|
||||
"@stitches/react": "^0.1.9",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"framer-motion": "^4.1.16",
|
||||
"next": "10.2.0",
|
||||
"perfect-freehand": "^0.4.7",
|
||||
"react": "17.0.2",
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { createSelectorHook, createState } from "@state-designer/react"
|
||||
import { clamp, screenToWorld } from "utils/utils"
|
||||
import { clamp, getCommonBounds, screenToWorld } from "utils/utils"
|
||||
import * as vec from "utils/vec"
|
||||
import { Data } from "types"
|
||||
import { Bounds, Data, Shape, ShapeType } from "types"
|
||||
import { defaultDocument } from "./data"
|
||||
import Shapes from "lib/shapes"
|
||||
import * as Sessions from "./sessions"
|
||||
|
||||
const initialData: Data = {
|
||||
|
@ -131,6 +132,42 @@ const state = createState({
|
|||
selectedIds(data) {
|
||||
return new Set(data.selectedIds)
|
||||
},
|
||||
selectedBounds(data) {
|
||||
const {
|
||||
selectedIds,
|
||||
currentPageId,
|
||||
document: { pages },
|
||||
} = data
|
||||
|
||||
return getCommonBounds(
|
||||
...Array.from(selectedIds.values())
|
||||
.map((id) => {
|
||||
const shape = pages[currentPageId].shapes[id]
|
||||
|
||||
switch (shape.type) {
|
||||
case ShapeType.Dot: {
|
||||
return Shapes[shape.type].getBounds(shape)
|
||||
}
|
||||
case ShapeType.Circle: {
|
||||
return Shapes[shape.type].getBounds(shape)
|
||||
}
|
||||
case ShapeType.Line: {
|
||||
return Shapes[shape.type].getBounds(shape)
|
||||
}
|
||||
case ShapeType.Polyline: {
|
||||
return Shapes[shape.type].getBounds(shape)
|
||||
}
|
||||
case ShapeType.Rectangle: {
|
||||
return Shapes[shape.type].getBounds(shape)
|
||||
}
|
||||
default: {
|
||||
return null
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(Boolean)
|
||||
)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
brushStroke: "rgba(0,0,0,.5)",
|
||||
hint: "rgba(66, 133, 244, 0.200)",
|
||||
selected: "rgba(66, 133, 244, 1.000)",
|
||||
bounds: "rgba(65, 132, 244, 1.000)",
|
||||
boundsBg: "rgba(65, 132, 244, 0.100)",
|
||||
},
|
||||
space: {},
|
||||
fontSizes: {
|
||||
|
@ -33,6 +35,11 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
zIndices: {},
|
||||
transitions: {},
|
||||
},
|
||||
utils: {
|
||||
zStrokeWidth: () => (value: number) => ({
|
||||
strokeWidth: `calc(${value}px / var(--camera-zoom))`,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
const light = theme({})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Data } from "types"
|
||||
import { Data, Bounds } from "types"
|
||||
import * as svg from "./svg"
|
||||
import * as vec from "./vec"
|
||||
|
||||
|
@ -6,6 +6,39 @@ export function screenToWorld(point: number[], data: Data) {
|
|||
return vec.sub(vec.div(point, data.camera.zoom), data.camera.point)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a bounding box that includes two bounding boxes.
|
||||
* @param a Bounding box
|
||||
* @param b Bounding box
|
||||
* @returns
|
||||
*/
|
||||
export function getExpandedBounds(a: Bounds, b: Bounds) {
|
||||
const minX = Math.min(a.minX, b.minX),
|
||||
minY = Math.min(a.minY, b.minY),
|
||||
maxX = Math.max(a.maxX, b.maxX),
|
||||
maxY = Math.max(a.maxY, b.maxY),
|
||||
width = Math.abs(maxX - minX),
|
||||
height = Math.abs(maxY - minY)
|
||||
|
||||
return { minX, minY, maxX, maxY, width, height }
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the common bounds of a group of bounds.
|
||||
* @returns
|
||||
*/
|
||||
export function getCommonBounds(...b: Bounds[]) {
|
||||
if (b.length < 2) return b[0]
|
||||
|
||||
let bounds = b[0]
|
||||
|
||||
for (let i = 1; i < b.length; i++) {
|
||||
bounds = getExpandedBounds(bounds, b[i])
|
||||
}
|
||||
|
||||
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])
|
||||
|
|
57
yarn.lock
57
yarn.lock
|
@ -939,6 +939,18 @@
|
|||
exec-sh "^0.3.2"
|
||||
minimist "^1.2.0"
|
||||
|
||||
"@emotion/is-prop-valid@^0.8.2":
|
||||
version "0.8.8"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a"
|
||||
integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==
|
||||
dependencies:
|
||||
"@emotion/memoize" "0.7.4"
|
||||
|
||||
"@emotion/memoize@0.7.4":
|
||||
version "0.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb"
|
||||
integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==
|
||||
|
||||
"@hapi/accept@5.0.1":
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.1.tgz#068553e867f0f63225a506ed74e899441af53e10"
|
||||
|
@ -3402,6 +3414,26 @@ fragment-cache@^0.2.1:
|
|||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
framer-motion@^4.1.16:
|
||||
version "4.1.16"
|
||||
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.16.tgz#dc715334847d0a146acf47f61019222d0d1c46c9"
|
||||
integrity sha512-sEc3UI3oncwE+RUzdd86TxbmpEaX/Ki/T0AmFYSsbxEqGZ3feLvzGL7BJlkhERIyyuAC9+OzI4BnhJM0GSUAMA==
|
||||
dependencies:
|
||||
framesync "5.3.0"
|
||||
hey-listen "^1.0.8"
|
||||
popmotion "9.3.6"
|
||||
style-value-types "4.1.4"
|
||||
tslib "^2.1.0"
|
||||
optionalDependencies:
|
||||
"@emotion/is-prop-valid" "^0.8.2"
|
||||
|
||||
framesync@5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/framesync/-/framesync-5.3.0.tgz#0ecfc955e8f5a6ddc8fdb0cc024070947e1a0d9b"
|
||||
integrity sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==
|
||||
dependencies:
|
||||
tslib "^2.1.0"
|
||||
|
||||
fs-extra@8.1.0:
|
||||
version "8.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
|
||||
|
@ -3652,6 +3684,11 @@ he@1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
hey-listen@^1.0.8:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
|
||||
integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==
|
||||
|
||||
hmac-drbg@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
|
@ -5622,6 +5659,16 @@ pnp-webpack-plugin@1.6.4:
|
|||
dependencies:
|
||||
ts-pnp "^1.1.6"
|
||||
|
||||
popmotion@9.3.6:
|
||||
version "9.3.6"
|
||||
resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-9.3.6.tgz#b5236fa28f242aff3871b9e23721f093133248d1"
|
||||
integrity sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==
|
||||
dependencies:
|
||||
framesync "5.3.0"
|
||||
hey-listen "^1.0.8"
|
||||
style-value-types "4.1.4"
|
||||
tslib "^2.1.0"
|
||||
|
||||
posix-character-classes@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
||||
|
@ -6734,6 +6781,14 @@ strip-json-comments@^3.0.1:
|
|||
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
style-value-types@4.1.4:
|
||||
version "4.1.4"
|
||||
resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-4.1.4.tgz#80f37cb4fb024d6394087403dfb275e8bb627e75"
|
||||
integrity sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==
|
||||
dependencies:
|
||||
hey-listen "^1.0.8"
|
||||
tslib "^2.1.0"
|
||||
|
||||
styled-jsx@3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018"
|
||||
|
@ -7051,7 +7106,7 @@ tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
|||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.3:
|
||||
tslib@^2.0.3, tslib@^2.1.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
||||
|
|
Loading…
Reference in a new issue