Merge branch 'main' of https://github.com/steveruizok/code-slate into main
This commit is contained in:
commit
79c254a938
16 changed files with 337 additions and 133 deletions
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false
|
||||||
|
}
|
|
@ -1,16 +1,17 @@
|
||||||
import * as React from "react"
|
import * as React from 'react'
|
||||||
import { Edge, Corner } from "types"
|
import { Edge, Corner } from 'types'
|
||||||
import { useSelector } from "state"
|
import { useSelector } from 'state'
|
||||||
import { getSelectedShapes, isMobile } from "utils/utils"
|
import { getSelectedShapes, isMobile } from 'utils/utils'
|
||||||
|
|
||||||
import CenterHandle from "./center-handle"
|
import CenterHandle from './center-handle'
|
||||||
import CornerHandle from "./corner-handle"
|
import CornerHandle from './corner-handle'
|
||||||
import EdgeHandle from "./edge-handle"
|
import EdgeHandle from './edge-handle'
|
||||||
import RotateHandle from "./rotate-handle"
|
import RotateHandle from './rotate-handle'
|
||||||
|
import Selected from '../selected'
|
||||||
|
|
||||||
export default function Bounds() {
|
export default function Bounds() {
|
||||||
const isBrushing = useSelector((s) => s.isIn("brushSelecting"))
|
const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
|
||||||
const isSelecting = useSelector((s) => s.isIn("selecting"))
|
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||||
const zoom = useSelector((s) => s.data.camera.zoom)
|
const zoom = useSelector((s) => s.data.camera.zoom)
|
||||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||||
const rotation = useSelector(({ data }) =>
|
const rotation = useSelector(({ data }) =>
|
||||||
|
@ -20,17 +21,18 @@ export default function Bounds() {
|
||||||
if (!bounds) return null
|
if (!bounds) return null
|
||||||
if (!isSelecting) return null
|
if (!isSelecting) return null
|
||||||
|
|
||||||
const size = (isMobile().any ? 16 : 8) / zoom // Touch target size
|
const size = (isMobile().any ? 12 : 8) / zoom // Touch target size
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g
|
<g
|
||||||
pointerEvents={isBrushing ? "none" : "all"}
|
pointerEvents={isBrushing ? 'none' : 'all'}
|
||||||
transform={`
|
transform={`
|
||||||
rotate(${rotation * (180 / Math.PI)},
|
rotate(${rotation * (180 / Math.PI)},
|
||||||
${(bounds.minX + bounds.maxX) / 2},
|
${(bounds.minX + bounds.maxX) / 2},
|
||||||
${(bounds.minY + bounds.maxY) / 2})
|
${(bounds.minY + bounds.maxY) / 2})
|
||||||
translate(${bounds.minX},${bounds.minY})`}
|
translate(${bounds.minX},${bounds.minY})`}
|
||||||
>
|
>
|
||||||
|
<Selected bounds={bounds} />
|
||||||
<CenterHandle bounds={bounds} />
|
<CenterHandle bounds={bounds} />
|
||||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
|
<EdgeHandle size={size} bounds={bounds} edge={Edge.Top} />
|
||||||
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
|
<EdgeHandle size={size} bounds={bounds} edge={Edge.Right} />
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import styled from "styles"
|
import styled from 'styles'
|
||||||
import React, { useCallback, useRef } from "react"
|
import state from 'state'
|
||||||
import useZoomEvents from "hooks/useZoomEvents"
|
import inputs from 'state/inputs'
|
||||||
import useCamera from "hooks/useCamera"
|
import React, { useCallback, useRef } from 'react'
|
||||||
import Page from "./page"
|
import useZoomEvents from 'hooks/useZoomEvents'
|
||||||
import Brush from "./brush"
|
import useCamera from 'hooks/useCamera'
|
||||||
import state from "state"
|
import Defs from './defs'
|
||||||
import Bounds from "./bounds/bounding-box"
|
import Page from './page'
|
||||||
import BoundsBg from "./bounds/bounds-bg"
|
import Brush from './brush'
|
||||||
import inputs from "state/inputs"
|
import Bounds from './bounds/bounding-box'
|
||||||
|
import BoundsBg from './bounds/bounds-bg'
|
||||||
|
|
||||||
export default function Canvas() {
|
export default function Canvas() {
|
||||||
const rCanvas = useRef<SVGSVGElement>(null)
|
const rCanvas = useRef<SVGSVGElement>(null)
|
||||||
|
@ -18,16 +19,16 @@ export default function Canvas() {
|
||||||
|
|
||||||
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||||
rCanvas.current.setPointerCapture(e.pointerId)
|
rCanvas.current.setPointerCapture(e.pointerId)
|
||||||
state.send("POINTED_CANVAS", inputs.pointerDown(e, "canvas"))
|
state.send('POINTED_CANVAS', inputs.pointerDown(e, 'canvas'))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
||||||
state.send("MOVED_POINTER", inputs.pointerMove(e))
|
state.send('MOVED_POINTER', inputs.pointerMove(e))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handlePointerUp = useCallback((e: React.PointerEvent) => {
|
const handlePointerUp = useCallback((e: React.PointerEvent) => {
|
||||||
rCanvas.current.releasePointerCapture(e.pointerId)
|
rCanvas.current.releasePointerCapture(e.pointerId)
|
||||||
state.send("STOPPED_POINTING", { id: "canvas", ...inputs.pointerUp(e) })
|
state.send('STOPPED_POINTING', { id: 'canvas', ...inputs.pointerUp(e) })
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -38,6 +39,7 @@ export default function Canvas() {
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
>
|
>
|
||||||
|
<Defs />
|
||||||
<MainGroup ref={rGroup}>
|
<MainGroup ref={rGroup}>
|
||||||
<BoundsBg />
|
<BoundsBg />
|
||||||
<Page />
|
<Page />
|
||||||
|
@ -48,18 +50,18 @@ export default function Canvas() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MainSVG = styled("svg", {
|
const MainSVG = styled('svg', {
|
||||||
position: "fixed",
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
width: "100%",
|
width: '100%',
|
||||||
height: "100%",
|
height: '100%',
|
||||||
touchAction: "none",
|
touchAction: 'none',
|
||||||
zIndex: 100,
|
zIndex: 100,
|
||||||
|
|
||||||
"& *": {
|
'& *': {
|
||||||
userSelect: "none",
|
userSelect: 'none',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const MainGroup = styled("g", {})
|
const MainGroup = styled('g', {})
|
||||||
|
|
25
components/canvas/defs.tsx
Normal file
25
components/canvas/defs.tsx
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { getShapeUtils } from "lib/shape-utils"
|
||||||
|
import { useSelector } from "state"
|
||||||
|
import { deepCompareArrays, getPage } from "utils/utils"
|
||||||
|
|
||||||
|
export default function Defs() {
|
||||||
|
const currentPageShapeIds = useSelector(({ data }) => {
|
||||||
|
return Object.values(getPage(data).shapes)
|
||||||
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
|
.map((shape) => shape.id)
|
||||||
|
}, deepCompareArrays)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<defs>
|
||||||
|
{currentPageShapeIds.map((id) => (
|
||||||
|
<Def key={id} id={id} />
|
||||||
|
))}
|
||||||
|
</defs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Def({ id }: { id: string }) {
|
||||||
|
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
||||||
|
|
||||||
|
return getShapeUtils(shape).render(shape)
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { useSelector } from "state"
|
import { useSelector } from 'state'
|
||||||
import { deepCompareArrays, getPage } from "utils/utils"
|
import { deepCompareArrays, getPage } from 'utils/utils'
|
||||||
import Shape from "./shape"
|
import Shape from './shape'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
On each state change, compare node ids of all shapes
|
On each state change, compare node ids of all shapes
|
||||||
|
@ -15,10 +15,12 @@ export default function Page() {
|
||||||
.map((shape) => shape.id)
|
.map((shape) => shape.id)
|
||||||
}, deepCompareArrays)
|
}, deepCompareArrays)
|
||||||
|
|
||||||
|
const isSelecting = useSelector((s) => s.isIn('selecting'))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{currentPageShapeIds.map((shapeId) => (
|
{currentPageShapeIds.map((shapeId) => (
|
||||||
<Shape key={shapeId} id={shapeId} />
|
<Shape key={shapeId} id={shapeId} isSelecting={isSelecting} />
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
58
components/canvas/selected.tsx
Normal file
58
components/canvas/selected.tsx
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import styled from 'styles'
|
||||||
|
import { useSelector } from 'state'
|
||||||
|
import {
|
||||||
|
deepCompareArrays,
|
||||||
|
getBoundsCenter,
|
||||||
|
getPage,
|
||||||
|
getSelectedShapes,
|
||||||
|
} from 'utils/utils'
|
||||||
|
import * as vec from 'utils/vec'
|
||||||
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
import { Bounds } from 'types'
|
||||||
|
import useShapeEvents from 'hooks/useShapeEvents'
|
||||||
|
import { useRef } from 'react'
|
||||||
|
|
||||||
|
export default function Selected({ bounds }: { bounds: Bounds }) {
|
||||||
|
const currentPageShapeIds = useSelector(({ data }) => {
|
||||||
|
return Array.from(data.selectedIds.values())
|
||||||
|
}, deepCompareArrays)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<g>
|
||||||
|
{currentPageShapeIds.map((id) => (
|
||||||
|
<ShapeOutline key={id} id={id} bounds={bounds} />
|
||||||
|
))}
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ShapeOutline({ id, bounds }: { id: string; bounds: Bounds }) {
|
||||||
|
const rIndicator = useRef<SVGUseElement>(null)
|
||||||
|
|
||||||
|
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
||||||
|
|
||||||
|
const shapeBounds = getShapeUtils(shape).getBounds(shape)
|
||||||
|
|
||||||
|
const events = useShapeEvents(id, rIndicator)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Indicator
|
||||||
|
ref={rIndicator}
|
||||||
|
as="use"
|
||||||
|
href={'#' + id}
|
||||||
|
transform={`rotate(${shape.rotation * (180 / Math.PI)},${getBoundsCenter(
|
||||||
|
shapeBounds
|
||||||
|
)}) translate(${vec.sub(shape.point, [bounds.minX, bounds.minY])})`}
|
||||||
|
{...events}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Indicator = styled('path', {
|
||||||
|
zStrokeWidth: 1,
|
||||||
|
strokeLineCap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: '$selected',
|
||||||
|
fill: 'transparent',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
})
|
|
@ -1,24 +1,25 @@
|
||||||
import React, { useCallback, useRef, memo } from "react"
|
import React, { useCallback, useRef, memo } from 'react'
|
||||||
import state, { useSelector } from "state"
|
import state, { useSelector } from 'state'
|
||||||
import inputs from "state/inputs"
|
import inputs from 'state/inputs'
|
||||||
import styled from "styles"
|
import styled from 'styles'
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { getPage } from "utils/utils"
|
import { getPage } from 'utils/utils'
|
||||||
|
import { ShapeStyles } from 'types'
|
||||||
function Shape({ id }: { id: string }) {
|
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
|
||||||
|
|
||||||
|
function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
||||||
const isHovered = useSelector((state) => state.data.hoveredId === id)
|
const isHovered = useSelector((state) => state.data.hoveredId === id)
|
||||||
|
|
||||||
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
const isSelected = useSelector((state) => state.values.selectedIds.has(id))
|
||||||
|
|
||||||
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
const shape = useSelector(({ data }) => getPage(data).shapes[id])
|
||||||
|
|
||||||
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
rGroup.current.setPointerCapture(e.pointerId)
|
rGroup.current.setPointerCapture(e.pointerId)
|
||||||
state.send("POINTED_SHAPE", inputs.pointerDown(e, id))
|
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
|
||||||
},
|
},
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
@ -27,27 +28,27 @@ function Shape({ id }: { id: string }) {
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
rGroup.current.releasePointerCapture(e.pointerId)
|
rGroup.current.releasePointerCapture(e.pointerId)
|
||||||
state.send("STOPPED_POINTING", inputs.pointerUp(e))
|
state.send('STOPPED_POINTING', inputs.pointerUp(e))
|
||||||
},
|
},
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerEnter = useCallback(
|
const handlePointerEnter = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
state.send("HOVERED_SHAPE", inputs.pointerEnter(e, id))
|
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
||||||
},
|
},
|
||||||
[id, shape]
|
[id, shape]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerMove = useCallback(
|
const handlePointerMove = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
state.send("MOVED_OVER_SHAPE", inputs.pointerEnter(e, id))
|
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
||||||
},
|
},
|
||||||
[id, shape]
|
[id, shape]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerLeave = useCallback(
|
const handlePointerLeave = useCallback(
|
||||||
() => state.send("UNHOVERED_SHAPE", { target: id }),
|
() => state.send('UNHOVERED_SHAPE', { target: id }),
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,47 +72,38 @@ function Shape({ id }: { id: string }) {
|
||||||
onPointerLeave={handlePointerLeave}
|
onPointerLeave={handlePointerLeave}
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
>
|
>
|
||||||
<defs>{getShapeUtils(shape).render(shape)}</defs>
|
{isSelecting && <HoverIndicator as="use" href={'#' + id} />}
|
||||||
<HoverIndicator as="use" xlinkHref={"#" + id} />
|
<StyledShape id={id} style={shape.style} />
|
||||||
<MainShape as="use" xlinkHref={"#" + id} {...shape.style} />
|
|
||||||
<Indicator as="use" xlinkHref={"#" + id} />
|
|
||||||
</StyledGroup>
|
</StyledGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const MainShape = styled("use", {
|
const StyledShape = memo(
|
||||||
|
({ id, style }: { id: string; style: ShapeStyles }) => {
|
||||||
|
return <MainShape as="use" href={'#' + id} {...style} />
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const MainShape = styled('use', {
|
||||||
zStrokeWidth: 1,
|
zStrokeWidth: 1,
|
||||||
})
|
})
|
||||||
|
|
||||||
const Indicator = styled("path", {
|
const HoverIndicator = styled('path', {
|
||||||
fill: "none",
|
fill: 'none',
|
||||||
stroke: "transparent",
|
stroke: 'transparent',
|
||||||
zStrokeWidth: 1,
|
pointerEvents: 'all',
|
||||||
pointerEvents: "none",
|
strokeLinecap: 'round',
|
||||||
strokeLineCap: "round",
|
strokeLinejoin: 'round',
|
||||||
strokeLinejoin: "round",
|
transform: 'all .2s',
|
||||||
})
|
})
|
||||||
|
|
||||||
const HoverIndicator = styled("path", {
|
const StyledGroup = styled('g', {
|
||||||
fill: "none",
|
|
||||||
stroke: "transparent",
|
|
||||||
pointerEvents: "all",
|
|
||||||
strokeLinecap: "round",
|
|
||||||
strokeLinejoin: "round",
|
|
||||||
transform: "all .2s",
|
|
||||||
})
|
|
||||||
|
|
||||||
const StyledGroup = styled("g", {
|
|
||||||
[`& ${HoverIndicator}`]: {
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: "0",
|
opacity: '0',
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
isSelected: {
|
isSelected: {
|
||||||
true: {
|
true: {},
|
||||||
[`& ${Indicator}`]: {
|
|
||||||
stroke: "$selected",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
false: {},
|
false: {},
|
||||||
},
|
},
|
||||||
isHovered: {
|
isHovered: {
|
||||||
|
@ -125,8 +117,8 @@ const StyledGroup = styled("g", {
|
||||||
isHovered: true,
|
isHovered: true,
|
||||||
css: {
|
css: {
|
||||||
[`& ${HoverIndicator}`]: {
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: "1",
|
opacity: '1',
|
||||||
stroke: "$hint",
|
stroke: '$hint',
|
||||||
zStrokeWidth: [8, 4],
|
zStrokeWidth: [8, 4],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -136,8 +128,8 @@ const StyledGroup = styled("g", {
|
||||||
isHovered: false,
|
isHovered: false,
|
||||||
css: {
|
css: {
|
||||||
[`& ${HoverIndicator}`]: {
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: "1",
|
opacity: '1',
|
||||||
stroke: "$hint",
|
stroke: '$hint',
|
||||||
zStrokeWidth: [6, 3],
|
zStrokeWidth: [6, 3],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -147,8 +139,8 @@ const StyledGroup = styled("g", {
|
||||||
isHovered: true,
|
isHovered: true,
|
||||||
css: {
|
css: {
|
||||||
[`& ${HoverIndicator}`]: {
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: "1",
|
opacity: '1',
|
||||||
stroke: "$hint",
|
stroke: '$hint',
|
||||||
zStrokeWidth: [8, 4],
|
zStrokeWidth: [8, 4],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -156,6 +148,6 @@ const StyledGroup = styled("g", {
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
export { Indicator, HoverIndicator }
|
export { HoverIndicator }
|
||||||
|
|
||||||
export default memo(Shape)
|
export default memo(Shape)
|
||||||
|
|
53
hooks/useShapeEvents.ts
Normal file
53
hooks/useShapeEvents.ts
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import { MutableRefObject, useCallback } from 'react'
|
||||||
|
import state from 'state'
|
||||||
|
import inputs from 'state/inputs'
|
||||||
|
|
||||||
|
export default function useShapeEvents(
|
||||||
|
id: string,
|
||||||
|
rGroup: MutableRefObject<SVGElement>
|
||||||
|
) {
|
||||||
|
const handlePointerDown = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
rGroup.current.setPointerCapture(e.pointerId)
|
||||||
|
state.send('POINTED_SHAPE', inputs.pointerDown(e, id))
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePointerUp = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
rGroup.current.releasePointerCapture(e.pointerId)
|
||||||
|
state.send('STOPPED_POINTING', inputs.pointerUp(e))
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePointerEnter = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePointerMove = useCallback(
|
||||||
|
(e: React.PointerEvent) => {
|
||||||
|
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
||||||
|
},
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const handlePointerLeave = useCallback(
|
||||||
|
() => state.send('UNHOVERED_SHAPE', { target: id }),
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
onPointerDown: handlePointerDown,
|
||||||
|
onPointerUp: handlePointerUp,
|
||||||
|
onPointerEnter: handlePointerEnter,
|
||||||
|
onPointerMove: handlePointerMove,
|
||||||
|
onPointerLeave: handlePointerLeave,
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import React, { useEffect, useRef } from "react"
|
||||||
import state from "state"
|
import state from "state"
|
||||||
import inputs from "state/inputs"
|
import inputs from "state/inputs"
|
||||||
import * as vec from "utils/vec"
|
import * as vec from "utils/vec"
|
||||||
|
import { usePinch } from "react-use-gesture"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture zoom gestures (pinches, wheels and pans) and send to the state.
|
* Capture zoom gestures (pinches, wheels and pans) and send to the state.
|
||||||
|
@ -65,5 +66,36 @@ export default function useZoomEvents(
|
||||||
}
|
}
|
||||||
}, [ref])
|
}, [ref])
|
||||||
|
|
||||||
return {}
|
const rPinchDa = useRef<number[] | undefined>(undefined)
|
||||||
|
const rPinchAngle = useRef<number>(undefined)
|
||||||
|
const rPinchPoint = useRef<number[] | undefined>(undefined)
|
||||||
|
|
||||||
|
const bind = usePinch(({ pinching, da, origin }) => {
|
||||||
|
if (!pinching) {
|
||||||
|
state.send("STOPPED_PINCHING")
|
||||||
|
rPinchDa.current = undefined
|
||||||
|
rPinchPoint.current = undefined
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rPinchPoint.current === undefined) {
|
||||||
|
state.send("STARTED_PINCHING")
|
||||||
|
rPinchDa.current = da
|
||||||
|
rPinchPoint.current = origin
|
||||||
|
}
|
||||||
|
|
||||||
|
const [distanceDelta, angleDelta] = vec.sub(rPinchDa.current, da)
|
||||||
|
|
||||||
|
state.send("PINCHED", {
|
||||||
|
delta: vec.sub(rPinchPoint.current, origin),
|
||||||
|
point: origin,
|
||||||
|
distanceDelta,
|
||||||
|
angleDelta,
|
||||||
|
})
|
||||||
|
|
||||||
|
rPinchDa.current = da
|
||||||
|
rPinchPoint.current = origin
|
||||||
|
})
|
||||||
|
|
||||||
|
return { ...bind() }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
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 { DrawShape, ShapeType } from "types"
|
import { DrawShape, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from "./index"
|
import { registerShapeUtils } from './index'
|
||||||
import { intersectPolylineBounds } from "utils/intersections"
|
import { intersectPolylineBounds } from 'utils/intersections'
|
||||||
import { boundsContainPolygon } from "utils/bounds"
|
import { boundsContainPolygon } from 'utils/bounds'
|
||||||
import getStroke from "perfect-freehand"
|
import getStroke from 'perfect-freehand'
|
||||||
import {
|
import {
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
getSvgPathFromStroke,
|
getSvgPathFromStroke,
|
||||||
translateBounds,
|
translateBounds,
|
||||||
} from "utils/utils"
|
} from 'utils/utils'
|
||||||
import { DotCircle } from "components/canvas/misc"
|
import { DotCircle } from 'components/canvas/misc'
|
||||||
import { shades } from "lib/colors"
|
import { shades } from 'lib/colors'
|
||||||
|
|
||||||
const pathCache = new WeakMap<DrawShape, string>([])
|
const pathCache = new WeakMap<number[][], string>([])
|
||||||
|
|
||||||
const draw = registerShapeUtils<DrawShape>({
|
const draw = registerShapeUtils<DrawShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
@ -23,8 +23,8 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
type: ShapeType.Draw,
|
type: ShapeType.Draw,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: "Draw",
|
name: 'Draw',
|
||||||
parentId: "page0",
|
parentId: 'page0',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
points: [[0, 0]],
|
points: [[0, 0]],
|
||||||
|
@ -32,10 +32,10 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
...props,
|
...props,
|
||||||
style: {
|
style: {
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
strokeLinecap: "round",
|
strokeLinecap: 'round',
|
||||||
strokeLinejoin: "round",
|
strokeLinejoin: 'round',
|
||||||
...props.style,
|
...props.style,
|
||||||
stroke: "transparent",
|
stroke: 'transparent',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -44,19 +44,18 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
const { id, point, points } = shape
|
const { id, point, points } = shape
|
||||||
|
|
||||||
if (points.length < 2) {
|
if (points.length < 2) {
|
||||||
return <DotCircle cx={point[0]} cy={point[1]} r={3} />
|
return <DotCircle id={id} cx={point[0]} cy={point[1]} r={3} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pathCache.has(shape)) {
|
if (!pathCache.has(points)) {
|
||||||
pathCache.set(shape, getSvgPathFromStroke(getStroke(points)))
|
pathCache.set(points, getSvgPathFromStroke(getStroke(points)))
|
||||||
}
|
}
|
||||||
|
|
||||||
return <path id={id} d={pathCache.get(shape)} />
|
return <path id={id} d={pathCache.get(points)} />
|
||||||
},
|
},
|
||||||
|
|
||||||
applyStyles(shape, style) {
|
applyStyles(shape, style) {
|
||||||
Object.assign(shape.style, style)
|
Object.assign(shape.style, style)
|
||||||
shape.style.fill = "transparent"
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-feather": "^2.0.9",
|
"react-feather": "^2.0.9",
|
||||||
|
"react-use-gesture": "^9.1.3",
|
||||||
"uuid": "^8.3.2"
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { current } from "immer"
|
import { current } from 'immer'
|
||||||
import { Data, DrawShape } from "types"
|
import { Data, DrawShape } from 'types'
|
||||||
import BaseSession from "./base-session"
|
import BaseSession from './base-session'
|
||||||
import { getShapeUtils } from "lib/shape-utils"
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { getPage, simplify } from "utils/utils"
|
import { getPage, simplify } from 'utils/utils'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
import commands from "state/commands"
|
import commands from 'state/commands'
|
||||||
|
|
||||||
export default class BrushSession extends BaseSession {
|
export default class BrushSession extends BaseSession {
|
||||||
origin: number[]
|
origin: number[]
|
||||||
|
@ -29,7 +29,7 @@ export default class BrushSession extends BaseSession {
|
||||||
update = (data: Data, point: number[]) => {
|
update = (data: Data, point: number[]) => {
|
||||||
const { shapeId } = this
|
const { shapeId } = this
|
||||||
|
|
||||||
const lp = vec.med(this.previous, point)
|
const lp = vec.med(this.previous, vec.toPrecision(point))
|
||||||
this.points.push(vec.sub(lp, this.origin))
|
this.points.push(vec.sub(lp, this.origin))
|
||||||
this.previous = lp
|
this.previous = lp
|
||||||
|
|
||||||
|
@ -46,15 +46,7 @@ export default class BrushSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
complete = (data: Data) => {
|
complete = (data: Data) => {
|
||||||
commands.draw(
|
commands.draw(data, this.shapeId, this.snapshot.points, this.points)
|
||||||
data,
|
|
||||||
this.shapeId,
|
|
||||||
this.snapshot.points,
|
|
||||||
simplify(this.points, 0.1 / data.camera.zoom).map(([x, y]) => [
|
|
||||||
Math.trunc(x * 100) / 100,
|
|
||||||
Math.trunc(y * 100) / 100,
|
|
||||||
])
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -130,6 +130,7 @@ const state = createState({
|
||||||
STRETCHED: "stretchSelection",
|
STRETCHED: "stretchSelection",
|
||||||
DISTRIBUTED: "distributeSelection",
|
DISTRIBUTED: "distributeSelection",
|
||||||
MOVED: "moveSelection",
|
MOVED: "moveSelection",
|
||||||
|
STARTED_PINCHING: { to: "pinching" },
|
||||||
},
|
},
|
||||||
initial: "notPointing",
|
initial: "notPointing",
|
||||||
states: {
|
states: {
|
||||||
|
@ -248,6 +249,12 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pinching: {
|
||||||
|
on: {
|
||||||
|
STOPPED_PINCHING: { to: "selecting" },
|
||||||
|
PINCHED: { do: "pinchCamera" },
|
||||||
|
},
|
||||||
|
},
|
||||||
draw: {
|
draw: {
|
||||||
initial: "creating",
|
initial: "creating",
|
||||||
states: {
|
states: {
|
||||||
|
@ -831,12 +838,31 @@ const state = createState({
|
||||||
|
|
||||||
setZoomCSS(camera.zoom)
|
setZoomCSS(camera.zoom)
|
||||||
},
|
},
|
||||||
panCamera(data, payload: { delta: number[]; point: number[] }) {
|
panCamera(data, payload: { delta: number[] }) {
|
||||||
const { camera } = data
|
const { camera } = data
|
||||||
data.camera.point = vec.sub(
|
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
|
||||||
camera.point,
|
},
|
||||||
vec.div(payload.delta, camera.zoom)
|
pinchCamera(
|
||||||
)
|
data,
|
||||||
|
payload: {
|
||||||
|
delta: number[]
|
||||||
|
distanceDelta: number
|
||||||
|
angleDelta: number
|
||||||
|
point: number[]
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { camera } = data
|
||||||
|
|
||||||
|
camera.point = vec.sub(camera.point, vec.div(payload.delta, camera.zoom))
|
||||||
|
|
||||||
|
const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
|
||||||
|
|
||||||
|
const p0 = screenToWorld(payload.point, data)
|
||||||
|
camera.zoom = clamp(next, 0.1, 3)
|
||||||
|
const p1 = screenToWorld(payload.point, data)
|
||||||
|
camera.point = vec.add(camera.point, vec.sub(p1, p0))
|
||||||
|
|
||||||
|
setZoomCSS(camera.zoom)
|
||||||
},
|
},
|
||||||
deleteSelectedIds(data) {
|
deleteSelectedIds(data) {
|
||||||
commands.deleteSelected(data)
|
commands.deleteSelected(data)
|
||||||
|
|
|
@ -45,7 +45,7 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
zStrokeWidth: () => (value: number | number[]) => {
|
zStrokeWidth: () => (value: number | number[]) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
return {
|
return {
|
||||||
strokeWidth: `calc(${value[0]} / var(--camera-zoom))`,
|
strokeWidth: `calc(${value[0]}px / var(--camera-zoom))`,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
// }
|
// }
|
||||||
|
|
||||||
return {
|
return {
|
||||||
strokeWidth: `calc(${value} / var(--camera-zoom))`,
|
strokeWidth: `calc(${value}px / var(--camera-zoom))`,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
11
utils/vec.ts
11
utils/vec.ts
|
@ -6,7 +6,7 @@
|
||||||
export function clamp(n: number, min: number): number
|
export function clamp(n: number, min: number): number
|
||||||
export function clamp(n: number, min: number, max: number): number
|
export function clamp(n: number, min: number, max: number): number
|
||||||
export function clamp(n: number, min: number, max?: number): number {
|
export function clamp(n: number, min: number, max?: number): number {
|
||||||
return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n)
|
return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -477,3 +477,12 @@ export function distanceToLineSegment(
|
||||||
export function nudge(A: number[], B: number[], d: number) {
|
export function nudge(A: number[], B: number[], d: number) {
|
||||||
return add(A, mul(uni(vec(A, B)), d))
|
return add(A, mul(uni(vec(A, B)), d))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Round a vector to a precision length.
|
||||||
|
* @param a
|
||||||
|
* @param n
|
||||||
|
*/
|
||||||
|
export function toPrecision(a: number[], n = 3) {
|
||||||
|
return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
|
||||||
|
}
|
||||||
|
|
|
@ -6697,6 +6697,11 @@ react-style-singleton@^2.1.0:
|
||||||
invariant "^2.2.4"
|
invariant "^2.2.4"
|
||||||
tslib "^1.0.0"
|
tslib "^1.0.0"
|
||||||
|
|
||||||
|
react-use-gesture@^9.1.3:
|
||||||
|
version "9.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-use-gesture/-/react-use-gesture-9.1.3.tgz#92bd143e4f58e69bd424514a5bfccba2a1d62ec0"
|
||||||
|
integrity sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==
|
||||||
|
|
||||||
react@17.0.2:
|
react@17.0.2:
|
||||||
version "17.0.2"
|
version "17.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||||
|
|
Loading…
Reference in a new issue