adds tooltips, improves arrow

This commit is contained in:
Steve Ruiz 2021-06-02 22:17:38 +01:00
parent 492d3e9769
commit 34256f992a
16 changed files with 359 additions and 167 deletions

View file

@ -31,7 +31,7 @@ export const IconButton = styled('button', {
variants: {
size: {
small: {
'& > svg': {
'& svg': {
height: '16px',
width: '16px',
},
@ -39,7 +39,7 @@ export const IconButton = styled('button', {
medium: {
height: 44,
width: 44,
'& > svg': {
'& svg': {
height: '20px',
width: '20px',
},
@ -47,7 +47,7 @@ export const IconButton = styled('button', {
large: {
height: 44,
width: 44,
'& > svg': {
'& svg': {
height: '24px',
width: '24px',
},

View file

@ -1,5 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared'
import Tooltip from 'components/tooltip'
import { strokes } from 'lib/shape-styles'
import { Square } from 'react-feather'
import state, { useSelector } from 'state'
@ -10,8 +11,10 @@ export default function QuickColorSelect() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="color">
<Square fill={strokes[color]} stroke={strokes[color]} />
<DropdownMenu.Trigger as={IconButton}>
<Tooltip label="Color">
<Square fill={strokes[color]} stroke={strokes[color]} />
</Tooltip>
</DropdownMenu.Trigger>
<ColorContent
onChange={(color) => state.send('CHANGED_STYLE', { color })}

View file

@ -1,5 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared'
import Tooltip from 'components/tooltip'
import state, { useSelector } from 'state'
import { DashStyle } from 'types'
import {
@ -21,8 +22,8 @@ export default function QuickdashSelect() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="dash">
{dashes[dash]}
<DropdownMenu.Trigger as={IconButton}>
<Tooltip label="Dash">{dashes[dash]}</Tooltip>
</DropdownMenu.Trigger>
<DropdownContent direction="vertical">
<DashItem isActive={dash === DashStyle.Solid} dash={DashStyle.Solid} />

View file

@ -1,5 +1,6 @@
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { IconButton } from 'components/shared'
import Tooltip from 'components/tooltip'
import { Circle } from 'react-feather'
import state, { useSelector } from 'state'
import { SizeStyle } from 'types'
@ -16,8 +17,10 @@ export default function QuickSizeSelect() {
return (
<DropdownMenu.Root>
<DropdownMenu.Trigger as={IconButton} title="size">
<Circle size={sizes[size]} stroke="none" fill="currentColor" />
<DropdownMenu.Trigger as={IconButton}>
<Tooltip label="Size">
<Circle size={sizes[size]} stroke="none" fill="currentColor" />
</Tooltip>
</DropdownMenu.Trigger>
<DropdownContent direction="vertical">
<SizeItem isActive={size === SizeStyle.Small} size={SizeStyle.Small} />

View file

@ -4,7 +4,7 @@ import * as Panel from 'components/panel'
import { useRef } from 'react'
import { IconButton } from 'components/shared'
import * as Checkbox from '@radix-ui/react-checkbox'
import { ChevronDown, Square, Trash2, X } from 'react-feather'
import { ChevronDown, Square, Tool, Trash2, X } from 'react-feather'
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
import { strokes } from 'lib/shape-styles'
import AlignDistribute from './align-distribute'
@ -35,6 +35,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import IsFilledPicker from './is-filled-picker'
import QuickSizeSelect from './quick-size-select'
import QuickdashSelect from './quick-dash-select'
import Tooltip from 'components/tooltip'
export default function StylePanel() {
const rContainer = useRef<HTMLDivElement>(null)
@ -54,7 +55,9 @@ export default function StylePanel() {
size="small"
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
>
<ChevronDown />
<Tooltip label="More">
<ChevronDown />
</Tooltip>
</IconButton>
</>
)}
@ -125,35 +128,49 @@ function SelectedShapeStyles() {
size="small"
onClick={() => state.send('DUPLICATED')}
>
<CopyIcon />
<Tooltip label="Duplicate">
<CopyIcon />
</Tooltip>
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('ROTATED_CCW')}
>
<RotateCounterClockwiseIcon />
<Tooltip label="Rotate">
<RotateCounterClockwiseIcon />
</Tooltip>
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_HIDE')}
>
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
<Tooltip label="Toogle Hidden">
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
</Tooltip>
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_LOCK')}
>
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
<Tooltip label="Toogle Locked">
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</Tooltip>
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('TOGGLED_SHAPE_ASPECT_LOCK')}
>
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
<Tooltip label="Toogle Aspect Ratio Lock">
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
</Tooltip>
</IconButton>
</ButtonsRow>
<ButtonsRow>
@ -162,35 +179,49 @@ function SelectedShapeStyles() {
size="small"
onClick={() => state.send('MOVED', { type: MoveType.ToBack })}
>
<PinBottomIcon />
<Tooltip label="Move to Back">
<PinBottomIcon />
</Tooltip>
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.Backward })}
>
<ArrowDownIcon />
<Tooltip label="Move Backward">
<ArrowDownIcon />
</Tooltip>
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.Forward })}
>
<ArrowUpIcon />
<Tooltip label="Move Forward">
<ArrowUpIcon />
</Tooltip>
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('MOVED', { type: MoveType.ToFront })}
>
<PinTopIcon />
<Tooltip label="More to Front">
<PinTopIcon />
</Tooltip>
</IconButton>
<IconButton
disabled={!hasSelection}
size="small"
onClick={() => state.send('DELETED')}
>
<Trash2 />
<Tooltip label="Delete">
<Trash2 size="15" />
</Tooltip>
</IconButton>
</ButtonsRow>
<AlignDistribute

View file

@ -18,6 +18,7 @@ import styled from 'styles'
import { ShapeType } from 'types'
import UndoRedo from './undo-redo'
import Zoom from './zoom'
import Tooltip from '../tooltip'
const selectArrowTool = () => state.send('SELECTED_ARROW_TOOL')
const selectCircleTool = () => state.send('SELECTED_CIRCLE_TOOL')
@ -32,20 +33,7 @@ const selectSelectTool = () => state.send('SELECTED_SELECT_TOOL')
const selectToolLock = () => state.send('TOGGLED_TOOL_LOCK')
export default function ToolsPanel() {
const activeTool = useSelector((state) =>
state.whenIn({
arrow: ShapeType.Arrow,
circle: ShapeType.Circle,
dot: ShapeType.Dot,
draw: ShapeType.Draw,
ellipse: ShapeType.Ellipse,
line: ShapeType.Line,
polyline: ShapeType.Polyline,
ray: ShapeType.Ray,
rectangle: ShapeType.Rectangle,
selecting: 'select',
})
)
const activeTool = useSelector((s) => s.data.activeTool)
const isToolLocked = useSelector((s) => s.data.settings.isToolLocked)
@ -56,48 +44,58 @@ export default function ToolsPanel() {
<Zoom />
<Flex size={{ '@sm': 'small' }}>
<Container>
<IconButton
name="select"
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
onClick={selectSelectTool}
isActive={activeTool === 'select'}
>
<CursorArrowIcon />
</IconButton>
<Tooltip label="Select">
<IconButton
name="select"
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
onClick={selectSelectTool}
isActive={activeTool === 'select'}
>
<CursorArrowIcon />
</IconButton>
</Tooltip>
</Container>
<Container>
<IconButton
name={ShapeType.Draw}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectDrawTool}
isActive={activeTool === ShapeType.Draw}
>
<Pencil1Icon />
</IconButton>
<IconButton
name={ShapeType.Rectangle}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectRectangleTool}
isActive={activeTool === ShapeType.Rectangle}
>
<SquareIcon />
</IconButton>
<IconButton
name={ShapeType.Circle}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectEllipseTool}
isActive={activeTool === ShapeType.Ellipse}
>
<CircleIcon />
</IconButton>
<IconButton
name={ShapeType.Arrow}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectArrowTool}
isActive={activeTool === ShapeType.Arrow}
>
<ArrowTopRightIcon />
</IconButton>
<Tooltip label="Draw">
<IconButton
name={ShapeType.Draw}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectDrawTool}
isActive={activeTool === ShapeType.Draw}
>
<Pencil1Icon />
</IconButton>
</Tooltip>
<Tooltip label="Rectangle">
<IconButton
name={ShapeType.Rectangle}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectRectangleTool}
isActive={activeTool === ShapeType.Rectangle}
>
<SquareIcon />
</IconButton>
</Tooltip>
<Tooltip label="Ellipse">
<IconButton
name={ShapeType.Circle}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectEllipseTool}
isActive={activeTool === ShapeType.Ellipse}
>
<CircleIcon />
</IconButton>
</Tooltip>
<Tooltip label="Arrow">
<IconButton
name={ShapeType.Arrow}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
onClick={selectArrowTool}
isActive={activeTool === ShapeType.Arrow}
>
<ArrowTopRightIcon />
</IconButton>
</Tooltip>
{/* <IconButton
name={ShapeType.Circle}
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
@ -132,19 +130,23 @@ export default function ToolsPanel() {
</IconButton> */}
</Container>
<Container>
<IconButton
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
onClick={selectToolLock}
>
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
{isPenLocked && (
<Tooltip label="Lock Tool">
<IconButton
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
onClick={selectToolLock}
>
<Pencil2Icon />
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
</IconButton>
</Tooltip>
{isPenLocked && (
<Tooltip label="Unlock Pen">
<IconButton
size={{ '@initial': 'small', '@sm': 'small', '@md': 'large' }}
onClick={selectToolLock}
>
<Pencil2Icon />
</IconButton>
</Tooltip>
)}
</Container>
</Flex>

View file

@ -2,6 +2,7 @@ import { IconButton } from 'components/shared'
import { RotateCcw, RotateCw, Trash2 } from 'react-feather'
import state, { useSelector } from 'state'
import styled from 'styles'
import Tooltip from '../tooltip'
const undo = () => state.send('UNDO')
const redo = () => state.send('REDO')
@ -10,15 +11,21 @@ const clear = () => state.send('CLEARED_PAGE')
export default function UndoRedo() {
return (
<Container size={{ '@sm': 'small' }}>
<IconButton onClick={undo}>
<RotateCcw />
</IconButton>
<IconButton onClick={redo}>
<RotateCw />
</IconButton>
<IconButton onClick={clear}>
<Trash2 />
</IconButton>
<Tooltip label="Undo">
<IconButton onClick={undo}>
<RotateCcw />
</IconButton>
</Tooltip>
<Tooltip label="Redo">
<IconButton onClick={redo}>
<RotateCw />
</IconButton>
</Tooltip>
<Tooltip label="Clear Canvas">
<IconButton onClick={clear}>
<Trash2 />
</IconButton>
</Tooltip>
</Container>
)
}

View file

@ -2,6 +2,7 @@ import { ZoomInIcon, ZoomOutIcon } from '@radix-ui/react-icons'
import { IconButton } from 'components/shared'
import state, { useSelector } from 'state'
import styled from 'styles'
import Tooltip from '../tooltip'
const zoomIn = () => state.send('ZOOMED_IN')
const zoomOut = () => state.send('ZOOMED_OUT')
@ -11,13 +12,19 @@ const zoomToActual = () => state.send('ZOOMED_TO_ACTUAL')
export default function Zoom() {
return (
<Container size={{ '@sm': 'small' }}>
<IconButton onClick={zoomOut}>
<ZoomOutIcon />
</IconButton>
<IconButton onClick={zoomIn}>
<ZoomInIcon />
</IconButton>
<ZoomCounter />
<Tooltip label="Zoom Out">
<IconButton onClick={zoomOut}>
<ZoomOutIcon />
</IconButton>
</Tooltip>
<Tooltip label="Zoom In">
<IconButton onClick={zoomIn}>
<ZoomInIcon />
</IconButton>
</Tooltip>
<Tooltip label="Reset Zoom">
<ZoomCounter />
</Tooltip>
</Container>
)
}

36
components/tooltip.tsx Normal file
View file

@ -0,0 +1,36 @@
import * as _Tooltip from '@radix-ui/react-tooltip'
import React from 'react'
import styled from 'styles'
export default function Tooltip({
children,
label,
side = 'top',
}: {
children: React.ReactNode
label: string
side?: 'bottom' | 'left' | 'right' | 'top'
}) {
return (
<_Tooltip.Root>
<_Tooltip.Trigger as="span">{children}</_Tooltip.Trigger>
<StyledContent side={side} sideOffset={8}>
{label}
<StyledArrow />
</StyledContent>
</_Tooltip.Root>
)
}
const StyledContent = styled(_Tooltip.Content, {
borderRadius: 3,
padding: '6px 12px',
fontSize: '$1',
backgroundColor: '$text',
color: '$panel',
})
const StyledArrow = styled(_Tooltip.Arrow, {
fill: '$text',
margin: '0 8px',
})

View file

@ -6,15 +6,11 @@ export const strokes: Record<ColorStyle, string> = {
[ColorStyle.LightGray]: 'rgba(224, 226, 230, 1.000)',
[ColorStyle.Gray]: 'rgba(172, 181, 189, 1.000)',
[ColorStyle.Black]: 'rgba(0,0,0, 1.000)',
[ColorStyle.Lime]: 'rgba(115, 184, 23, 1.000)',
[ColorStyle.Green]: 'rgba(54, 178, 77, 1.000)',
[ColorStyle.Teal]: 'rgba(9, 167, 120, 1.000)',
[ColorStyle.Cyan]: 'rgba(14, 152, 173, 1.000)',
[ColorStyle.Blue]: 'rgba(28, 126, 214, 1.000)',
[ColorStyle.Indigo]: 'rgba(66, 99, 235, 1.000)',
[ColorStyle.Violet]: 'rgba(112, 72, 232, 1.000)',
[ColorStyle.Grape]: 'rgba(174, 62, 200, 1.000)',
[ColorStyle.Pink]: 'rgba(214, 51, 108, 1.000)',
[ColorStyle.Red]: 'rgba(240, 63, 63, 1.000)',
[ColorStyle.Orange]: 'rgba(247, 103, 6, 1.000)',
[ColorStyle.Yellow]: 'rgba(245, 159, 0, 1.000)',
@ -24,16 +20,12 @@ export const fills = {
[ColorStyle.White]: 'rgba(224, 226, 230, 1.000)',
[ColorStyle.LightGray]: 'rgba(255, 255, 255, 1.000)',
[ColorStyle.Gray]: 'rgba(224, 226, 230, 1.000)',
[ColorStyle.Black]: 'rgba(224, 226, 230, 1.000)',
[ColorStyle.Lime]: 'rgba(243, 252, 227, 1.000)',
[ColorStyle.Black]: 'rgba(255, 255, 255, 1.000)',
[ColorStyle.Green]: 'rgba(235, 251, 238, 1.000)',
[ColorStyle.Teal]: 'rgba(230, 252, 245, 1.000)',
[ColorStyle.Cyan]: 'rgba(227, 250, 251, 1.000)',
[ColorStyle.Blue]: 'rgba(231, 245, 255, 1.000)',
[ColorStyle.Indigo]: 'rgba(237, 242, 255, 1.000)',
[ColorStyle.Violet]: 'rgba(242, 240, 255, 1.000)',
[ColorStyle.Grape]: 'rgba(249, 240, 252, 1.000)',
[ColorStyle.Pink]: 'rgba(254, 241, 246, 1.000)',
[ColorStyle.Red]: 'rgba(255, 245, 245, 1.000)',
[ColorStyle.Orange]: 'rgba(255, 244, 229, 1.000)',
[ColorStyle.Yellow]: 'rgba(255, 249, 219, 1.000)',

View file

@ -97,44 +97,60 @@ const arrow = registerShapeUtils<ArrowShape>({
const { start, end, bend: _bend } = handles
const arrowDist = vec.dist(start.point, end.point)
const bendDist = arrowDist * bend
const showCircle = Math.abs(bendDist) > 20
const showCircle = !vec.isEqual(
_bend.point,
vec.med(start.point, end.point)
)
const style = getShapeStyle(shape.style)
let body: JSX.Element
let endAngle: number
if (showCircle) {
if (!ctpCache.has(handles)) {
ctpCache.set(
handles,
circleFromThreePoints(start.point, end.point, _bend.point)
)
}
const circle = getCtp(shape)
body = (
<path
d={getArrowArcPath(start, end, circle, bend)}
fill="none"
strokeLinecap="round"
/>
)
const CE =
vec.angle([circle[0], circle[1]], end.point) -
vec.angle(start.point, end.point) +
(Math.PI / 2) * (bend > 0 ? 0.98 : -0.98)
endAngle = CE
} else {
body = (
<polyline
points={[start.point, end.point].join(' ')}
strokeLinecap="round"
/>
)
endAngle = 0
}
// Arrowhead
const length = Math.min(arrowDist / 2, 16 + +style.strokeWidth * 2)
const angle = showCircle ? bend * (Math.PI * 0.48) : 0
const u = vec.uni(vec.vec(start.point, end.point))
const v = vec.rot(vec.mul(vec.neg(u), length), angle)
const v = vec.rot(vec.mul(vec.neg(u), length), endAngle)
const b = vec.add(points[1], vec.rot(v, Math.PI / 6))
const c = vec.add(points[1], vec.rot(v, -(Math.PI / 6)))
if (showCircle && !ctpCache.has(handles)) {
ctpCache.set(
handles,
circleFromThreePoints(start.point, end.point, _bend.point)
)
}
const circle = showCircle && getCtp(shape)
return (
<g id={id}>
{circle ? (
<>
<path
d={getArrowArcPath(start, end, circle, bend)}
fill="none"
strokeLinecap="round"
/>
</>
) : (
<polyline
points={[start.point, end.point].join(' ')}
strokeLinecap="round"
/>
)}
{body}
<circle
cx={start.point[0]}
cy={start.point[1]}
@ -301,11 +317,11 @@ function getArrowArcPath(
}
function getBendPoint(shape: ArrowShape) {
const { start, end, bend } = shape.handles
const { start, end } = shape.handles
const dist = vec.dist(start.point, end.point)
const midPoint = vec.med(start.point, end.point)
const bendDist = (dist / 2) * shape.bend
const bendDist = (dist / 2) * shape.bend * Math.min(1, dist / 128)
const u = vec.uni(vec.vec(start.point, end.point))
return Math.abs(bendDist) < 10

View file

@ -1,6 +1,6 @@
import { v4 as uuid } from 'uuid'
import * as vec from 'utils/vec'
import { DashStyle, DrawShape, ShapeType } from 'types'
import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
import { registerShapeUtils } from './index'
import { intersectPolylineBounds } from 'utils/intersections'
import { boundsContainPolygon } from 'utils/bounds'
@ -11,7 +11,6 @@ import {
getSvgPathFromStroke,
translateBounds,
} from 'utils/utils'
import styled from 'styles'
import { defaultStyle, getShapeStyle } from 'lib/shape-styles'
const pathCache = new WeakMap<DrawShape['points'], string>([])
@ -48,17 +47,7 @@ const draw = registerShapeUtils<DrawShape>({
const styles = getShapeStyle(style)
if (!pathCache.has(points)) {
pathCache.set(
points,
getSvgPathFromStroke(
getStroke(points, {
size: +styles.strokeWidth * 2,
thinning: 0.9,
end: { taper: 100 },
start: { taper: 40 },
})
)
)
renderPath(shape, style)
}
if (points.length < 2) {
@ -155,9 +144,11 @@ const draw = registerShapeUtils<DrawShape>({
},
applyStyles(shape, style) {
Object.assign(shape.style, style)
shape.style.isFilled = false
shape.style.dash = DashStyle.Solid
const styles = { ...shape.style, ...style }
styles.isFilled = false
styles.dash = DashStyle.Solid
shape.style = styles
shape.points = [...shape.points]
return this
},
@ -166,6 +157,18 @@ const draw = registerShapeUtils<DrawShape>({
export default draw
const DrawPath = styled('path', {
strokeWidth: 0,
})
function renderPath(shape: DrawShape, style: ShapeStyles) {
const styles = getShapeStyle(style)
pathCache.set(
shape.points,
getSvgPathFromStroke(
getStroke(shape.points, {
size: +styles.strokeWidth * 2,
thinning: 0.9,
end: { taper: 100 },
start: { taper: 40 },
})
)
)
}

View file

@ -13,6 +13,7 @@
"@radix-ui/react-dropdown-menu": "^0.0.19",
"@radix-ui/react-icons": "^1.0.3",
"@radix-ui/react-radio-group": "^0.0.16",
"@radix-ui/react-tooltip": "^0.0.18",
"@state-designer/react": "^1.7.1",
"@stitches/react": "^0.1.9",
"framer-motion": "^4.1.16",

View file

@ -58,6 +58,7 @@ const initialData: Data = {
point: [0, 0],
zoom: 1,
},
activeTool: 'select',
brush: undefined,
boundsRotation: 0,
pointedId: null,
@ -142,6 +143,7 @@ const state = createState({
initial: 'selecting',
states: {
selecting: {
onEnter: 'setActiveToolSelect',
on: {
SAVED: 'forceSave',
UNDO: 'undo',
@ -322,6 +324,7 @@ const state = createState({
},
states: {
draw: {
onEnter: 'setActiveToolDraw',
initial: 'creating',
states: {
creating: {
@ -361,6 +364,7 @@ const state = createState({
},
},
dot: {
onEnter: 'setActiveToolDot',
initial: 'creating',
states: {
creating: {
@ -417,6 +421,7 @@ const state = createState({
},
},
arrow: {
onEnter: 'setActiveToolArrow',
initial: 'creating',
states: {
creating: {
@ -462,6 +467,7 @@ const state = createState({
},
},
circle: {
onEnter: 'setActiveToolCircle',
initial: 'creating',
states: {
creating: {
@ -492,6 +498,7 @@ const state = createState({
},
},
ellipse: {
onEnter: 'setActiveToolEllipse',
initial: 'creating',
states: {
creating: {
@ -519,6 +526,7 @@ const state = createState({
},
},
rectangle: {
onEnter: 'setActiveToolRectangle',
initial: 'creating',
states: {
creating: {
@ -549,6 +557,7 @@ const state = createState({
},
},
ray: {
onEnter: 'setActiveToolRay',
initial: 'creating',
states: {
creating: {
@ -579,6 +588,7 @@ const state = createState({
},
},
line: {
onEnter: 'setActiveToolLine',
initial: 'creating',
states: {
creating: {
@ -608,7 +618,9 @@ const state = createState({
},
},
},
polyline: {},
polyline: {
onEnter: 'setActiveToolPolyline',
},
},
},
drawingShape: {
@ -1011,6 +1023,42 @@ const state = createState({
commands.rotateCcw(data)
},
/* ---------------------- Tool ---------------------- */
setActiveTool(data, payload: { tool: ShapeType | 'select' }) {
data.activeTool = payload.tool
},
setActiveToolSelect(data) {
data.activeTool = 'select'
},
setActiveToolDraw(data) {
data.activeTool = ShapeType.Draw
},
setActiveToolRectangle(data) {
data.activeTool = ShapeType.Rectangle
},
setActiveToolEllipse(data) {
data.activeTool = ShapeType.Ellipse
},
setActiveToolArrow(data) {
data.activeTool = ShapeType.Arrow
},
setActiveToolDot(data) {
data.activeTool = ShapeType.Dot
},
setActiveToolPolyline(data) {
data.activeTool = ShapeType.Polyline
},
setActiveToolRay(data) {
data.activeTool = ShapeType.Ray
},
setActiveToolCircle(data) {
data.activeTool = ShapeType.Circle
},
setActiveToolLine(data) {
data.activeTool = ShapeType.Line
},
/* --------------------- Camera --------------------- */
zoomIn(data) {
@ -1019,7 +1067,7 @@ const state = createState({
const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data)
camera.zoom = Math.min(3, (i + 1) * 0.25)
camera.zoom = getCameraZoom((i + 1) * 0.25)
const p1 = screenToWorld(center, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
@ -1031,7 +1079,7 @@ const state = createState({
const center = [window.innerWidth / 2, window.innerHeight / 2]
const p0 = screenToWorld(center, data)
camera.zoom = Math.max(0.1, (i - 1) * 0.25)
camera.zoom = getCameraZoom((i - 1) * 0.25)
const p1 = screenToWorld(center, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
@ -1067,10 +1115,11 @@ const state = createState({
const bounds = getSelectedBounds(data)
const zoom =
const zoom = getCameraZoom(
bounds.width > bounds.height
? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height
)
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
@ -1096,10 +1145,11 @@ const state = createState({
)
)
const zoom =
const zoom = getCameraZoom(
bounds.width > bounds.height
? (window.innerWidth - 128) / bounds.width
: (window.innerHeight - 128) / bounds.height
)
const mx = (window.innerWidth - bounds.width * zoom) / 2 / zoom
const my = (window.innerHeight - bounds.height * zoom) / 2 / zoom
@ -1114,7 +1164,7 @@ const state = createState({
const next = camera.zoom - (payload.delta / 100) * camera.zoom
const p0 = screenToWorld(payload.point, data)
camera.zoom = clamp(next, 0.1, 3)
camera.zoom = getCameraZoom(next)
const p1 = screenToWorld(payload.point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
@ -1140,7 +1190,7 @@ const state = createState({
const next = camera.zoom - (payload.distanceDelta / 300) * camera.zoom
const p0 = screenToWorld(payload.point, data)
camera.zoom = clamp(next, 0.1, 3)
camera.zoom = getCameraZoom(next)
const p1 = screenToWorld(payload.point, data)
camera.point = vec.add(camera.point, vec.sub(p1, p0))
@ -1336,3 +1386,7 @@ let session: Sessions.BaseSession
export default state
export const useSelector = createSelectorHook(state)
function getCameraZoom(zoom: number) {
return clamp(zoom, 0.1, 5)
}

View file

@ -23,6 +23,7 @@ export interface Data {
point: number[]
zoom: number
}
activeTool: ShapeType | 'select'
brush?: Bounds
boundsRotation: number
selectedIds: Set<string>
@ -72,15 +73,11 @@ export enum ColorStyle {
LightGray = 'LightGray',
Gray = 'Gray',
Black = 'Black',
Lime = 'Lime',
Green = 'Green',
Teal = 'Teal',
Cyan = 'Cyan',
Blue = 'Blue',
Indigo = 'Indigo',
Violet = 'Violet',
Grape = 'Grape',
Pink = 'Pink',
Red = 'Red',
Orange = 'Orange',
Yellow = 'Yellow',

View file

@ -1500,6 +1500,29 @@
"@radix-ui/primitive" "0.0.5"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-tooltip@^0.0.18":
version "0.0.18"
resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-0.0.18.tgz#7594297dbc2acf101ac45fdb414c7bc0ac9426bc"
integrity sha512-oYRAbbTZJ8zEokrrk5pe7QbzF+ZnzMby8mBplrkColi0ntToJ7RKzPgUs1OOFvmg/Nld0Iy2FufrTNlyyEI3kQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.0.5"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-context" "0.0.5"
"@radix-ui/react-id" "0.0.6"
"@radix-ui/react-polymorphic" "0.0.11"
"@radix-ui/react-popper" "0.0.16"
"@radix-ui/react-portal" "0.0.13"
"@radix-ui/react-presence" "0.0.14"
"@radix-ui/react-primitive" "0.0.13"
"@radix-ui/react-slot" "0.0.10"
"@radix-ui/react-use-controllable-state" "0.0.6"
"@radix-ui/react-use-escape-keydown" "0.0.6"
"@radix-ui/react-use-layout-effect" "0.0.5"
"@radix-ui/react-use-previous" "0.0.5"
"@radix-ui/react-use-rect" "0.0.7"
"@radix-ui/react-visually-hidden" "0.0.13"
"@radix-ui/react-use-body-pointer-events@0.0.6":
version "0.0.6"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.6.tgz#30b21301880417e7dbb345871ff5a83f2abe0d8d"
@ -1538,6 +1561,13 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-previous@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-0.0.5.tgz#75191d1fa0ac24c560fe8cfbaa2f1174858cbb2f"
integrity sha512-GjtJlWlDAEMqCm2RDnVdWI6tk4/ZQfRq/VlP05Xy5rFZj6lD37VZWVWUELMBasRPzd2AS/9wPmphOgjH0VnE5A==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-rect@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-0.0.7.tgz#e3a55fa7183ef436042198787bf38f8c9befcc14"
@ -1553,6 +1583,15 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-visually-hidden@0.0.13":
version "0.0.13"
resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-0.0.13.tgz#c7f69097eb7d796dcd9117cdd228d87991c08baf"
integrity sha512-8VNuE4/3PnyrLv1je56fxaa5qka0Nb6/FlyQEDF2HCPpxVOWR4sxRfSBe8cjy+Me+pJN9ZoKBIuoFCVRk54xJA==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-polymorphic" "0.0.11"
"@radix-ui/react-primitive" "0.0.13"
"@radix-ui/rect@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-0.0.5.tgz#6000d8d800288114af4bbc5863e6b58755d7d978"