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 Page from "./page"
|
||||||
import Brush from "./brush"
|
import Brush from "./brush"
|
||||||
import state from "state"
|
import state from "state"
|
||||||
|
import Bounds from "./bounds"
|
||||||
|
import BoundsBg from "./bounds-bg"
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const rCanvas = useRef<SVGSVGElement>(null)
|
const rCanvas = useRef<SVGSVGElement>(null)
|
||||||
|
@ -37,7 +39,9 @@ export default function Canvas() {
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
>
|
>
|
||||||
<MainGroup ref={rGroup}>
|
<MainGroup ref={rGroup}>
|
||||||
|
<BoundsBg />
|
||||||
<Page />
|
<Page />
|
||||||
|
<Bounds />
|
||||||
<Brush />
|
<Brush />
|
||||||
</MainGroup>
|
</MainGroup>
|
||||||
</MainSVG>
|
</MainSVG>
|
||||||
|
|
|
@ -1,19 +1,8 @@
|
||||||
import React, { useCallback, useRef } from "react"
|
import React, { useCallback, useRef, memo } from "react"
|
||||||
import state, { useSelector } from "state"
|
import state, { useSelector } from "state"
|
||||||
import styled from "styles"
|
|
||||||
import { getPointerEventInfo } from "utils/utils"
|
import { getPointerEventInfo } from "utils/utils"
|
||||||
import { memo } from "react"
|
import shapes from "lib/shapes"
|
||||||
import Shapes from "lib/shapes"
|
import styled from "styles"
|
||||||
|
|
||||||
/*
|
|
||||||
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
|
|
||||||
*/
|
|
||||||
|
|
||||||
function Shape({ id }: { id: string }) {
|
function Shape({ id }: { id: string }) {
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
@ -66,7 +55,7 @@ function Shape({ id }: { id: string }) {
|
||||||
onPointerLeave={handlePointerLeave}
|
onPointerLeave={handlePointerLeave}
|
||||||
>
|
>
|
||||||
<defs>
|
<defs>
|
||||||
{Shapes[shape.type] ? Shapes[shape.type].render(shape) : null}
|
{shapes[shape.type] ? shapes[shape.type].render(shape) : null}
|
||||||
</defs>
|
</defs>
|
||||||
<HoverIndicator as="use" xlinkHref={"#" + id} />
|
<HoverIndicator as="use" xlinkHref={"#" + id} />
|
||||||
<use xlinkHref={"#" + id} {...shape.style} />
|
<use xlinkHref={"#" + id} {...shape.style} />
|
||||||
|
@ -78,7 +67,7 @@ function Shape({ id }: { id: string }) {
|
||||||
const Indicator = styled("path", {
|
const Indicator = styled("path", {
|
||||||
fill: "none",
|
fill: "none",
|
||||||
stroke: "transparent",
|
stroke: "transparent",
|
||||||
strokeWidth: "max(1, calc(2 / var(--camera-zoom)))",
|
zStrokeWidth: 1,
|
||||||
pointerEvents: "none",
|
pointerEvents: "none",
|
||||||
strokeLineCap: "round",
|
strokeLineCap: "round",
|
||||||
strokeLinejoin: "round",
|
strokeLinejoin: "round",
|
||||||
|
@ -87,7 +76,7 @@ const Indicator = styled("path", {
|
||||||
const HoverIndicator = styled("path", {
|
const HoverIndicator = styled("path", {
|
||||||
fill: "none",
|
fill: "none",
|
||||||
stroke: "transparent",
|
stroke: "transparent",
|
||||||
strokeWidth: "max(1, calc(8 / var(--camera-zoom)))",
|
zStrokeWidth: 8,
|
||||||
pointerEvents: "all",
|
pointerEvents: "all",
|
||||||
strokeLinecap: "round",
|
strokeLinecap: "round",
|
||||||
strokeLinejoin: "round",
|
strokeLinejoin: "round",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import * as vec from "utils/vec"
|
import * as vec from "utils/vec"
|
||||||
import { BaseLibShape, CircleShape, ShapeType } from "types"
|
import { BaseLibShape, CircleShape, ShapeType } from "types"
|
||||||
|
import { boundsCache } from "./index"
|
||||||
|
|
||||||
const Circle: BaseLibShape<ShapeType.Circle> = {
|
const Circle: BaseLibShape<ShapeType.Circle> = {
|
||||||
create(props): CircleShape {
|
create(props): CircleShape {
|
||||||
|
@ -23,19 +24,26 @@ const Circle: BaseLibShape<ShapeType.Circle> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
|
if (boundsCache.has(shape)) {
|
||||||
|
return boundsCache.get(shape)
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
point: [cx, cy],
|
point: [x, y],
|
||||||
radius,
|
radius,
|
||||||
} = shape
|
} = shape
|
||||||
|
|
||||||
return {
|
const bounds = {
|
||||||
minX: cx,
|
minX: x,
|
||||||
maxX: cx + radius * 2,
|
maxX: x + radius * 2,
|
||||||
minY: cy,
|
minY: y,
|
||||||
maxY: cy + radius * 2,
|
maxY: y + radius * 2,
|
||||||
width: radius * 2,
|
width: radius * 2,
|
||||||
height: radius * 2,
|
height: radius * 2,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boundsCache.set(shape, bounds)
|
||||||
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, test) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import * as vec from "utils/vec"
|
import * as vec from "utils/vec"
|
||||||
import { BaseLibShape, DotShape, ShapeType } from "types"
|
import { BaseLibShape, DotShape, ShapeType } from "types"
|
||||||
|
import { boundsCache } from "./index"
|
||||||
|
|
||||||
const Dot: BaseLibShape<ShapeType.Dot> = {
|
const Dot: BaseLibShape<ShapeType.Dot> = {
|
||||||
create(props): DotShape {
|
create(props): DotShape {
|
||||||
|
@ -22,18 +23,25 @@ const Dot: BaseLibShape<ShapeType.Dot> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
|
if (boundsCache.has(shape)) {
|
||||||
|
return boundsCache.get(shape)
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
point: [cx, cy],
|
point: [x, y],
|
||||||
} = shape
|
} = shape
|
||||||
|
|
||||||
return {
|
const bounds = {
|
||||||
minX: cx,
|
minX: x,
|
||||||
maxX: cx + 4,
|
maxX: x + 8,
|
||||||
minY: cy,
|
minY: y,
|
||||||
maxY: cy + 4,
|
maxY: y + 8,
|
||||||
width: 4,
|
width: 8,
|
||||||
height: 4,
|
height: 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boundsCache.set(shape, bounds)
|
||||||
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, test) {
|
||||||
|
|
|
@ -3,11 +3,15 @@ import Dot from "./dot"
|
||||||
import Polyline from "./polyline"
|
import Polyline from "./polyline"
|
||||||
import Rectangle from "./rectangle"
|
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.Circle]: Circle,
|
||||||
[ShapeType.Dot]: Dot,
|
[ShapeType.Dot]: Dot,
|
||||||
[ShapeType.Polyline]: Polyline,
|
[ShapeType.Polyline]: Polyline,
|
||||||
[ShapeType.Rectangle]: Rectangle,
|
[ShapeType.Rectangle]: Rectangle,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default shapes
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import * as vec from "utils/vec"
|
import * as vec from "utils/vec"
|
||||||
import { BaseLibShape, PolylineShape, ShapeType } from "types"
|
import { BaseLibShape, PolylineShape, ShapeType } from "types"
|
||||||
|
import { boundsCache } from "./index"
|
||||||
|
|
||||||
const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
||||||
create(props): PolylineShape {
|
create(props): PolylineShape {
|
||||||
|
@ -23,6 +24,10 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
|
if (boundsCache.has(shape)) {
|
||||||
|
return boundsCache.get(shape)
|
||||||
|
}
|
||||||
|
|
||||||
let minX = 0
|
let minX = 0
|
||||||
let minY = 0
|
let minY = 0
|
||||||
let maxX = 0
|
let maxX = 0
|
||||||
|
@ -35,7 +40,7 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
||||||
maxY = Math.max(y, maxY)
|
maxY = Math.max(y, maxY)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
const bounds = {
|
||||||
minX: minX + shape.point[0],
|
minX: minX + shape.point[0],
|
||||||
minY: minY + shape.point[1],
|
minY: minY + shape.point[1],
|
||||||
maxX: maxX + shape.point[0],
|
maxX: maxX + shape.point[0],
|
||||||
|
@ -43,6 +48,9 @@ const Polyline: BaseLibShape<ShapeType.Polyline> = {
|
||||||
width: maxX - minX,
|
width: maxX - minX,
|
||||||
height: maxY - minY,
|
height: maxY - minY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boundsCache.set(shape, bounds)
|
||||||
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape) {
|
hitTest(shape) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { v4 as uuid } from "uuid"
|
import { v4 as uuid } from "uuid"
|
||||||
import * as vec from "utils/vec"
|
import * as vec from "utils/vec"
|
||||||
import { BaseLibShape, RectangleShape, ShapeType } from "types"
|
import { BaseLibShape, RectangleShape, ShapeType } from "types"
|
||||||
|
import { boundsCache } from "./index"
|
||||||
|
|
||||||
const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
||||||
create(props): RectangleShape {
|
create(props): RectangleShape {
|
||||||
|
@ -23,12 +24,16 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
|
if (boundsCache.has(shape)) {
|
||||||
|
return boundsCache.get(shape)
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
point: [x, y],
|
point: [x, y],
|
||||||
size: [width, height],
|
size: [width, height],
|
||||||
} = shape
|
} = shape
|
||||||
|
|
||||||
return {
|
const bounds = {
|
||||||
minX: x,
|
minX: x,
|
||||||
maxX: x + width,
|
maxX: x + width,
|
||||||
minY: y,
|
minY: y,
|
||||||
|
@ -36,6 +41,9 @@ const Rectangle: BaseLibShape<ShapeType.Rectangle> = {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boundsCache.set(shape, bounds)
|
||||||
|
return bounds
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape) {
|
hitTest(shape) {
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"@state-designer/react": "^1.7.1",
|
"@state-designer/react": "^1.7.1",
|
||||||
"@stitches/react": "^0.1.9",
|
"@stitches/react": "^0.1.9",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
|
"framer-motion": "^4.1.16",
|
||||||
"next": "10.2.0",
|
"next": "10.2.0",
|
||||||
"perfect-freehand": "^0.4.7",
|
"perfect-freehand": "^0.4.7",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { createSelectorHook, createState } from "@state-designer/react"
|
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 * as vec from "utils/vec"
|
||||||
import { Data } from "types"
|
import { Bounds, Data, Shape, ShapeType } from "types"
|
||||||
import { defaultDocument } from "./data"
|
import { defaultDocument } from "./data"
|
||||||
|
import Shapes from "lib/shapes"
|
||||||
import * as Sessions from "./sessions"
|
import * as Sessions from "./sessions"
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
|
@ -131,6 +132,42 @@ const state = createState({
|
||||||
selectedIds(data) {
|
selectedIds(data) {
|
||||||
return new Set(data.selectedIds)
|
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)",
|
brushStroke: "rgba(0,0,0,.5)",
|
||||||
hint: "rgba(66, 133, 244, 0.200)",
|
hint: "rgba(66, 133, 244, 0.200)",
|
||||||
selected: "rgba(66, 133, 244, 1.000)",
|
selected: "rgba(66, 133, 244, 1.000)",
|
||||||
|
bounds: "rgba(65, 132, 244, 1.000)",
|
||||||
|
boundsBg: "rgba(65, 132, 244, 0.100)",
|
||||||
},
|
},
|
||||||
space: {},
|
space: {},
|
||||||
fontSizes: {
|
fontSizes: {
|
||||||
|
@ -33,6 +35,11 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
zIndices: {},
|
zIndices: {},
|
||||||
transitions: {},
|
transitions: {},
|
||||||
},
|
},
|
||||||
|
utils: {
|
||||||
|
zStrokeWidth: () => (value: number) => ({
|
||||||
|
strokeWidth: `calc(${value}px / var(--camera-zoom))`,
|
||||||
|
}),
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const light = theme({})
|
const light = theme({})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Data } from "types"
|
import { Data, Bounds } from "types"
|
||||||
import * as svg from "./svg"
|
import * as svg from "./svg"
|
||||||
import * as vec from "./vec"
|
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)
|
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[]) {
|
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])
|
||||||
|
|
57
yarn.lock
57
yarn.lock
|
@ -939,6 +939,18 @@
|
||||||
exec-sh "^0.3.2"
|
exec-sh "^0.3.2"
|
||||||
minimist "^1.2.0"
|
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":
|
"@hapi/accept@5.0.1":
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.1.tgz#068553e867f0f63225a506ed74e899441af53e10"
|
resolved "https://registry.yarnpkg.com/@hapi/accept/-/accept-5.0.1.tgz#068553e867f0f63225a506ed74e899441af53e10"
|
||||||
|
@ -3402,6 +3414,26 @@ fragment-cache@^0.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
map-cache "^0.2.2"
|
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:
|
fs-extra@8.1.0:
|
||||||
version "8.1.0"
|
version "8.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0"
|
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"
|
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
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:
|
hmac-drbg@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
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:
|
dependencies:
|
||||||
ts-pnp "^1.1.6"
|
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:
|
posix-character-classes@^0.1.0:
|
||||||
version "0.1.1"
|
version "0.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
|
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"
|
resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006"
|
||||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
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:
|
styled-jsx@3.3.2:
|
||||||
version "3.3.2"
|
version "3.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-3.3.2.tgz#2474601a26670a6049fb4d3f94bd91695b3ce018"
|
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"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||||
|
|
||||||
tslib@^2.0.3:
|
tslib@^2.0.3, tslib@^2.1.0:
|
||||||
version "2.2.0"
|
version "2.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.2.0.tgz#fb2c475977e35e241311ede2693cee1ec6698f5c"
|
||||||
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
integrity sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue