From fe3980c80c46d1baa0e5106bd8a0d10879b6bab5 Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Fri, 28 May 2021 21:30:27 +0100 Subject: [PATCH] Adds tool lock --- components/canvas/shape.tsx | 27 +- components/panel.tsx | 121 ++--- components/style-panel/align-distribute.tsx | 42 +- components/style-panel/color-picker.tsx | 96 ++-- components/style-panel/style-panel.tsx | 106 +++-- components/style-panel/width-picker.tsx | 82 ++++ components/toolbar.tsx | 135 +++--- hooks/useKeyboardEvents.ts | 154 +++--- lib/colors.ts | 60 +-- lib/shape-utils/circle.tsx | 37 +- lib/shape-utils/dot.tsx | 26 +- lib/shape-utils/draw.tsx | 14 +- lib/shape-utils/ellipse.tsx | 36 +- lib/shape-utils/line.tsx | 26 +- lib/shape-utils/polyline.tsx | 24 +- lib/shape-utils/ray.tsx | 26 +- lib/shape-utils/rectangle.tsx | 39 +- package.json | 1 + state/commands/duplicate.ts | 48 ++ state/commands/index.ts | 30 +- state/commands/nudge.ts | 40 ++ state/commands/transform-single.ts | 20 +- state/state.ts | 492 +++++++++++--------- styles/stitches.config.ts | 57 +-- types.ts | 52 ++- utils/utils.ts | 52 +-- utils/vec.ts | 2 +- yarn.lock | 29 ++ 28 files changed, 1136 insertions(+), 738 deletions(-) create mode 100644 components/style-panel/width-picker.tsx create mode 100644 state/commands/duplicate.ts create mode 100644 state/commands/nudge.ts diff --git a/components/canvas/shape.tsx b/components/canvas/shape.tsx index 5c1096d49..f49628167 100644 --- a/components/canvas/shape.tsx +++ b/components/canvas/shape.tsx @@ -23,9 +23,9 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) { // detects the change and pulls this component. if (!shape) return null + const center = getShapeUtils(shape).getCenter(shape) const transform = ` - rotate(${shape.rotation * (180 / Math.PI)}, - ${getShapeUtils(shape).getCenter(shape)}) + rotate(${shape.rotation * (180 / Math.PI)}, ${center}) translate(${shape.point})` return ( @@ -38,18 +38,37 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) { > {isSelecting && } + {/* + + {center.toString()} + */} ) } const StyledShape = memo( ({ id, style }: { id: string; style: ShapeStyles }) => { - return + return ( + + ) } ) const MainShape = styled('use', { - zStrokeWidth: 1, + // zStrokeWidth: 1, }) const HoverIndicator = styled('path', { diff --git a/components/panel.tsx b/components/panel.tsx index 7078d3ce5..e2fe1d1dd 100644 --- a/components/panel.tsx +++ b/components/panel.tsx @@ -1,20 +1,20 @@ -import styled from "styles" +import styled from 'styles' -export const Root = styled("div", { - position: "relative", - backgroundColor: "$panel", - borderRadius: "4px", - overflow: "hidden", - border: "1px solid $border", - pointerEvents: "all", - userSelect: "none", +export const Root = styled('div', { + position: 'relative', + backgroundColor: '$panel', + borderRadius: '4px', + overflow: 'hidden', + border: '1px solid $border', + pointerEvents: 'all', + userSelect: 'none', zIndex: 200, - boxShadow: "0px 2px 25px rgba(0,0,0,.16)", + boxShadow: '0px 2px 25px rgba(0,0,0,.16)', variants: { isOpen: { true: { - width: "auto", + width: 'auto', minWidth: 300, }, false: { @@ -25,63 +25,74 @@ export const Root = styled("div", { }, }) -export const Layout = styled("div", { - display: "grid", - gridTemplateColumns: "1fr", - gridTemplateRows: "auto 1fr", - gridAutoRows: "28px", - height: "100%", - width: "auto", - minWidth: "100%", +export const Layout = styled('div', { + display: 'grid', + gridTemplateColumns: '1fr', + gridTemplateRows: 'auto 1fr', + gridAutoRows: '28px', + height: '100%', + width: 'auto', + minWidth: '100%', maxWidth: 560, - overflow: "hidden", - userSelect: "none", - pointerEvents: "all", + overflow: 'hidden', + userSelect: 'none', + pointerEvents: 'all', }) -export const Header = styled("div", { - pointerEvents: "all", - display: "flex", - width: "100%", - alignItems: "center", - justifyContent: "space-between", - borderBottom: "1px solid $border", - position: "relative", +export const Header = styled('div', { + pointerEvents: 'all', + display: 'flex', + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', + borderBottom: '1px solid $border', + position: 'relative', - "& h3": { - position: "absolute", + '& h3': { + position: 'absolute', top: 0, left: 0, - width: "100%", - height: "100%", - textAlign: "center", + width: '100%', + height: '100%', + textAlign: 'center', padding: 0, margin: 0, - display: "flex", - justifyContent: "center", - alignItems: "center", - fontSize: "13px", - pointerEvents: "none", - userSelect: "none", + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: '13px', + pointerEvents: 'none', + userSelect: 'none', + }, + + variants: { + side: { + left: { + flexDirection: 'row', + }, + right: { + flexDirection: 'row-reverse', + }, + }, }, }) -export const ButtonsGroup = styled("div", { - display: "flex", +export const ButtonsGroup = styled('div', { + display: 'flex', }) -export const Content = styled("div", { - position: "relative", - pointerEvents: "all", - overflowY: "scroll", +export const Content = styled('div', { + position: 'relative', + pointerEvents: 'all', + overflowY: 'scroll', }) -export const Footer = styled("div", { - overflowX: "scroll", - color: "$text", - font: "$debug", - padding: "0 12px", - display: "flex", - alignItems: "center", - borderTop: "1px solid $border", +export const Footer = styled('div', { + overflowX: 'scroll', + color: '$text', + font: '$debug', + padding: '0 12px', + display: 'flex', + alignItems: 'center', + borderTop: '1px solid $border', }) diff --git a/components/style-panel/align-distribute.tsx b/components/style-panel/align-distribute.tsx index 55416ab0a..eeb8ea27d 100644 --- a/components/style-panel/align-distribute.tsx +++ b/components/style-panel/align-distribute.tsx @@ -9,50 +9,50 @@ import { SpaceEvenlyVerticallyIcon, StretchHorizontallyIcon, StretchVerticallyIcon, -} from "@radix-ui/react-icons" -import { IconButton } from "components/shared" -import state from "state" -import styled from "styles" -import { AlignType, DistributeType, StretchType } from "types" +} from '@radix-ui/react-icons' +import { IconButton } from 'components/shared' +import state from 'state' +import styled from 'styles' +import { AlignType, DistributeType, StretchType } from 'types' function alignTop() { - state.send("ALIGNED", { type: AlignType.Top }) + state.send('ALIGNED', { type: AlignType.Top }) } function alignCenterVertical() { - state.send("ALIGNED", { type: AlignType.CenterVertical }) + state.send('ALIGNED', { type: AlignType.CenterVertical }) } function alignBottom() { - state.send("ALIGNED", { type: AlignType.Bottom }) + state.send('ALIGNED', { type: AlignType.Bottom }) } function stretchVertically() { - state.send("STRETCHED", { type: StretchType.Vertical }) + state.send('STRETCHED', { type: StretchType.Vertical }) } function distributeVertically() { - state.send("DISTRIBUTED", { type: DistributeType.Vertical }) + state.send('DISTRIBUTED', { type: DistributeType.Vertical }) } function alignLeft() { - state.send("ALIGNED", { type: AlignType.Left }) + state.send('ALIGNED', { type: AlignType.Left }) } function alignCenterHorizontal() { - state.send("ALIGNED", { type: AlignType.CenterHorizontal }) + state.send('ALIGNED', { type: AlignType.CenterHorizontal }) } function alignRight() { - state.send("ALIGNED", { type: AlignType.Right }) + state.send('ALIGNED', { type: AlignType.Right }) } function stretchHorizontally() { - state.send("STRETCHED", { type: StretchType.Horizontal }) + state.send('STRETCHED', { type: StretchType.Horizontal }) } function distributeHorizontally() { - state.send("DISTRIBUTED", { type: DistributeType.Horizontal }) + state.send('DISTRIBUTED', { type: DistributeType.Horizontal }) } export default function AlignDistribute({ @@ -98,15 +98,15 @@ export default function AlignDistribute({ ) } -const Container = styled("div", { - display: "grid", +const Container = styled('div', { + display: 'grid', padding: 4, - gridTemplateColumns: "repeat(5, auto)", + gridTemplateColumns: 'repeat(5, auto)', [`& ${IconButton}`]: { - color: "$text", + color: '$text', }, [`& ${IconButton} > svg`]: { - fill: "red", - stroke: "transparent", + fill: 'red', + stroke: 'transparent', }, }) diff --git a/components/style-panel/color-picker.tsx b/components/style-panel/color-picker.tsx index 7d74e0d44..f22ab2c8e 100644 --- a/components/style-panel/color-picker.tsx +++ b/components/style-panel/color-picker.tsx @@ -1,6 +1,6 @@ -import * as DropdownMenu from "@radix-ui/react-dropdown-menu" -import { Square } from "react-feather" -import styled from "styles" +import * as DropdownMenu from '@radix-ui/react-dropdown-menu' +import { Square } from 'react-feather' +import styled from 'styles' interface Props { label: string @@ -13,7 +13,7 @@ export default function ColorPicker({ label, color, colors, onChange }: Props) { return ( -

{label}

+
@@ -31,96 +31,96 @@ function ColorIcon({ color }: { color: string }) { return ( ) } const Colors = styled(DropdownMenu.Content, { - display: "grid", + display: 'grid', padding: 4, - gridTemplateColumns: "repeat(6, 1fr)", - border: "1px solid $border", - backgroundColor: "$panel", + gridTemplateColumns: 'repeat(6, 1fr)', + border: '1px solid $border', + backgroundColor: '$panel', borderRadius: 4, - boxShadow: "0px 5px 15px -5px hsla(206,22%,7%,.15)", + boxShadow: '0px 5px 15px -5px hsla(206,22%,7%,.15)', }) const ColorButton = styled(DropdownMenu.Item, { - position: "relative", - cursor: "pointer", + position: 'relative', + cursor: 'pointer', height: 32, width: 32, - border: "none", - padding: "none", - background: "none", - display: "flex", - alignItems: "center", - justifyContent: "center", + border: 'none', + padding: 'none', + background: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', - "&::before": { + '&::before': { content: "''", - position: "absolute", + position: 'absolute', top: 4, left: 4, right: 4, bottom: 4, - pointerEvents: "none", + pointerEvents: 'none', zIndex: 0, }, - "&:hover::before": { - backgroundColor: "$hover", + '&:hover::before': { + backgroundColor: '$hover', borderRadius: 4, }, - "& svg": { - position: "relative", - stroke: "rgba(0,0,0,.2)", + '& svg': { + position: 'relative', + stroke: 'rgba(0,0,0,.2)', strokeWidth: 1, zIndex: 1, }, }) const CurrentColor = styled(DropdownMenu.Trigger, { - position: "relative", - display: "flex", - width: "100%", - background: "none", - border: "none", - cursor: "pointer", - outline: "none", - alignItems: "center", - justifyContent: "space-between", - padding: "4px 6px 4px 12px", + position: 'relative', + display: 'flex', + width: '100%', + background: 'none', + border: 'none', + cursor: 'pointer', + outline: 'none', + alignItems: 'center', + justifyContent: 'space-between', + padding: '4px 6px 4px 12px', - "&::before": { + '&::before': { content: "''", - position: "absolute", + position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, - pointerEvents: "none", + pointerEvents: 'none', zIndex: -1, }, - "&:hover::before": { - backgroundColor: "$hover", + '&:hover::before': { + backgroundColor: '$hover', borderRadius: 4, }, - "& h3": { - fontFamily: "$ui", - fontSize: "$2", - fontWeight: "$1", + '& label': { + fontFamily: '$ui', + fontSize: '$2', + fontWeight: '$1', margin: 0, padding: 0, }, - "& svg": { - position: "relative", - stroke: "rgba(0,0,0,.2)", + '& svg': { + position: 'relative', + stroke: 'rgba(0,0,0,.2)', strokeWidth: 1, zIndex: 1, }, diff --git a/components/style-panel/style-panel.tsx b/components/style-panel/style-panel.tsx index f92fda422..0dba65482 100644 --- a/components/style-panel/style-panel.tsx +++ b/components/style-panel/style-panel.tsx @@ -1,15 +1,17 @@ -import styled from "styles" -import state, { useSelector } from "state" -import * as Panel from "components/panel" -import { useRef } from "react" -import { IconButton } from "components/shared" -import { Circle, Trash, X } from "react-feather" -import { deepCompare, deepCompareArrays, getSelectedShapes } from "utils/utils" -import { shades, fills, strokes } from "lib/colors" +import styled from 'styles' +import state, { useSelector } from 'state' +import * as Panel from 'components/panel' +import { useRef } from 'react' +import { IconButton } from 'components/shared' +import { Circle, Copy, Lock, Trash, Unlock, X } from 'react-feather' +import { deepCompare, deepCompareArrays, getSelectedShapes } from 'utils/utils' +import { shades, fills, strokes } from 'lib/colors' -import ColorPicker from "./color-picker" -import AlignDistribute from "./align-distribute" -import { ShapeStyles } from "types" +import ColorPicker from './color-picker' +import AlignDistribute from './align-distribute' +import { ShapeStyles } from 'types' +import WidthPicker from './width-picker' +import { CopyIcon } from '@radix-ui/react-icons' const fillColors = { ...shades, ...fills } const strokeColors = { ...shades, ...strokes } @@ -23,7 +25,7 @@ export default function StylePanel() { {isOpen ? ( ) : ( - state.send("TOGGLED_STYLE_PANEL_OPEN")}> + state.send('TOGGLED_STYLE_PANEL_OPEN')}> )} @@ -72,17 +74,9 @@ function SelectedShapeStyles({}: {}) { return ( - +

Style

- - state.send("DELETED")} - > - - - - state.send("TOGGLED_STYLE_PANEL_OPEN")}> + state.send('TOGGLED_STYLE_PANEL_OPEN')}>
@@ -91,18 +85,40 @@ function SelectedShapeStyles({}: {}) { label="Fill" color={shapesStyle.fill} colors={fillColors} - onChange={(color) => state.send("CHANGED_STYLE", { fill: color })} + onChange={(color) => state.send('CHANGED_STYLE', { fill: color })} /> state.send("CHANGED_STYLE", { stroke: color })} + onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })} /> + + + + 1} hasThreeOrMore={selectedIds.length > 2} /> + + state.send('DELETED')} + > + + + state.send('DUPLICATED')} + > + + + + + + +
) @@ -112,8 +128,8 @@ const StylePanelRoot = styled(Panel.Root, { minWidth: 1, width: 184, maxWidth: 184, - overflow: "hidden", - position: "relative", + overflow: 'hidden', + position: 'relative', variants: { isOpen: { @@ -129,3 +145,41 @@ const StylePanelRoot = styled(Panel.Root, { const Content = styled(Panel.Content, { padding: 8, }) + +const Row = styled('div', { + position: 'relative', + display: 'flex', + width: '100%', + background: 'none', + border: 'none', + cursor: 'pointer', + outline: 'none', + alignItems: 'center', + justifyContent: 'space-between', + padding: '4px 2px 4px 12px', + + '& label': { + fontFamily: '$ui', + fontSize: '$2', + fontWeight: '$1', + margin: 0, + padding: 0, + }, + + '& > svg': { + position: 'relative', + }, +}) + +const ButtonsRow = styled('div', { + position: 'relative', + display: 'flex', + width: '100%', + background: 'none', + border: 'none', + cursor: 'pointer', + outline: 'none', + alignItems: 'center', + justifyContent: 'flex-start', + padding: 4, +}) diff --git a/components/style-panel/width-picker.tsx b/components/style-panel/width-picker.tsx new file mode 100644 index 000000000..8db571544 --- /dev/null +++ b/components/style-panel/width-picker.tsx @@ -0,0 +1,82 @@ +import { DotsHorizontalIcon } from '@radix-ui/react-icons' +import * as RadioGroup from '@radix-ui/react-radio-group' +import { IconButton } from 'components/shared' +import { ChangeEvent } from 'react' +import { Circle } from 'react-feather' +import state from 'state' +import styled from 'styles' + +function setWidth(e: ChangeEvent) { + state.send('CHANGED_STYLE', { + strokeWidth: Number(e.currentTarget.value), + }) +} + +export default function WidthPicker({ + strokeWidth = 2, +}: { + strokeWidth?: number +}) { + return ( + + + + + + + + + + + + ) +} + +const Group = styled(RadioGroup.Root, { + display: 'flex', +}) + +const RadioItem = styled(RadioGroup.Item, { + height: '32px', + width: '32px', + backgroundColor: '$panel', + borderRadius: '4px', + padding: '0', + margin: '0', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + outline: 'none', + border: 'none', + pointerEvents: 'all', + cursor: 'pointer', + + '&:hover:not(:disabled)': { + backgroundColor: '$hover', + '& svg': { + fill: '$text', + strokeWidth: '0', + }, + }, + + '&:disabled': { + opacity: '0.5', + }, + + variants: { + isActive: { + true: { + '& svg': { + fill: '$text', + strokeWidth: '0', + }, + }, + false: { + '& svg': { + fill: '$inactive', + strokeWidth: '0', + }, + }, + }, + }, +}) diff --git a/components/toolbar.tsx b/components/toolbar.tsx index 3c37501b3..9cad5cc5d 100644 --- a/components/toolbar.tsx +++ b/components/toolbar.tsx @@ -1,134 +1,147 @@ -import state, { useSelector } from "state" -import styled from "styles" -import { Menu } from "react-feather" +import state, { useSelector } from 'state' +import styled from 'styles' +import { Lock, Menu, Unlock } from 'react-feather' +import { IconButton } from './shared' export default function Toolbar() { const activeTool = useSelector((state) => state.whenIn({ - selecting: "select", - dot: "dot", - circle: "circle", - ellipse: "ellipse", - ray: "ray", - line: "line", - polyline: "polyline", - rectangle: "rectangle", - draw: "draw", + selecting: 'select', + dot: 'dot', + circle: 'circle', + ellipse: 'ellipse', + ray: 'ray', + line: 'line', + polyline: 'polyline', + rectangle: 'rectangle', + draw: 'draw', }) ) + const isToolLocked = useSelector((s) => s.data.settings.isToolLocked) + return (
+ - +
- - + +
) } -const ToolbarContainer = styled("div", { - gridArea: "toolbar", - userSelect: "none", - borderBottom: "1px solid black", - display: "flex", - alignItems: "center", - justifyContent: "space-between", - backgroundColor: "$panel", +const ToolbarContainer = styled('div', { + gridArea: 'toolbar', + userSelect: 'none', + borderBottom: '1px solid black', + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + backgroundColor: '$panel', gap: 8, - fontSize: "$1", + fontSize: '$1', zIndex: 200, }) -const Section = styled("div", { - whiteSpace: "nowrap", - overflow: "hidden", - display: "flex", +const Section = styled('div', { + whiteSpace: 'nowrap', + overflowY: 'hidden', + overflowX: 'auto', + display: 'flex', + scrollbarWidth: 'none', + '&::-webkit-scrollbar': { + '-webkit-appearance': 'none', + width: 0, + height: 0, + }, }) -const Button = styled("button", { - display: "flex", - alignItems: "center", - cursor: "pointer", - font: "$ui", - fontSize: "$ui", - height: "40px", - outline: "none", +const Button = styled('button', { + display: 'flex', + alignItems: 'center', + cursor: 'pointer', + font: '$ui', + fontSize: '$ui', + height: '40px', + outline: 'none', borderRadius: 0, - border: "none", - padding: "0 12px", - background: "none", - "&:hover": { - backgroundColor: "$hint", + border: 'none', + padding: '0 12px', + background: 'none', + '&:hover': { + backgroundColor: '$hint', }, - "& svg": { + '& svg': { height: 16, width: 16, }, variants: { isSelected: { true: { - color: "$selected", + color: '$selected', }, false: {}, }, diff --git a/hooks/useKeyboardEvents.ts b/hooks/useKeyboardEvents.ts index 280198de3..104257601 100644 --- a/hooks/useKeyboardEvents.ts +++ b/hooks/useKeyboardEvents.ts @@ -1,197 +1,217 @@ -import { useEffect } from "react" -import state from "state" -import { MoveType } from "types" -import { getKeyboardEventInfo, metaKey } from "utils/utils" +import { useEffect } from 'react' +import state from 'state' +import { MoveType } from 'types' +import { getKeyboardEventInfo, metaKey } from 'utils/utils' export default function useKeyboardEvents() { useEffect(() => { function handleKeyDown(e: KeyboardEvent) { - if (metaKey(e) && !["i", "r", "j"].includes(e.key)) { + if (metaKey(e) && !['i', 'r', 'j'].includes(e.key)) { e.preventDefault() } switch (e.key) { - case "!": { + case 'ArrowUp': { + state.send('NUDGED', { delta: [0, -1], ...getKeyboardEventInfo(e) }) + break + } + case 'ArrowRight': { + state.send('NUDGED', { delta: [1, 0], ...getKeyboardEventInfo(e) }) + break + } + case 'ArrowDown': { + state.send('NUDGED', { delta: [0, 1], ...getKeyboardEventInfo(e) }) + break + } + case 'ArrowLeft': { + state.send('NUDGED', { delta: [-1, 0], ...getKeyboardEventInfo(e) }) + break + } + case '!': { // Shift + 1 if (e.shiftKey) { - state.send("ZOOMED_TO_FIT") + state.send('ZOOMED_TO_FIT') } break } - case "@": { + case '@': { // Shift + 2 if (e.shiftKey) { - state.send("ZOOMED_TO_SELECTION") + state.send('ZOOMED_TO_SELECTION') } break } - case ")": { + case ')': { // Shift + 0 if (e.shiftKey) { - state.send("ZOOMED_TO_ACTUAL") + state.send('ZOOMED_TO_ACTUAL') } break } - case "Escape": { - state.send("CANCELLED") + case 'Escape': { + state.send('CANCELLED') break } - case "z": { + case 'z': { if (metaKey(e)) { if (e.shiftKey) { - state.send("REDO", getKeyboardEventInfo(e)) + state.send('REDO', getKeyboardEventInfo(e)) } else { - state.send("UNDO", getKeyboardEventInfo(e)) + state.send('UNDO', getKeyboardEventInfo(e)) } } break } - case "‘": { + case '‘': { if (metaKey(e)) { - state.send("MOVED", { + state.send('MOVED', { ...getKeyboardEventInfo(e), type: MoveType.ToFront, }) } break } - case "“": { + case '“': { if (metaKey(e)) { - state.send("MOVED", { + state.send('MOVED', { ...getKeyboardEventInfo(e), type: MoveType.ToBack, }) } break } - case "]": { + case ']': { if (metaKey(e)) { - state.send("MOVED", { + state.send('MOVED', { ...getKeyboardEventInfo(e), type: MoveType.Forward, }) } break } - case "[": { + case '[': { if (metaKey(e)) { - state.send("MOVED", { + state.send('MOVED', { ...getKeyboardEventInfo(e), type: MoveType.Backward, }) } break } - case "Shift": { - state.send("PRESSED_SHIFT_KEY", getKeyboardEventInfo(e)) + case 'Shift': { + state.send('PRESSED_SHIFT_KEY', getKeyboardEventInfo(e)) break } - case "Alt": { - state.send("PRESSED_ALT_KEY", getKeyboardEventInfo(e)) + case 'Alt': { + state.send('PRESSED_ALT_KEY', getKeyboardEventInfo(e)) break } - case "Backspace": { - state.send("DELETED", getKeyboardEventInfo(e)) + case 'Backspace': { + state.send('DELETED', getKeyboardEventInfo(e)) break } - case "s": { + case 's': { if (metaKey(e)) { - state.send("SAVED", getKeyboardEventInfo(e)) + state.send('SAVED', getKeyboardEventInfo(e)) } break } - case "a": { + case 'a': { if (metaKey(e)) { - state.send("SELECTED_ALL", getKeyboardEventInfo(e)) + state.send('SELECTED_ALL', getKeyboardEventInfo(e)) } break } - case "v": { + case 'v': { if (metaKey(e)) { - state.send("PASTED", getKeyboardEventInfo(e)) + state.send('PASTED', getKeyboardEventInfo(e)) } else { - state.send("SELECTED_SELECT_TOOL", getKeyboardEventInfo(e)) + state.send('SELECTED_SELECT_TOOL', getKeyboardEventInfo(e)) } break } - case "d": { - state.send("SELECTED_DRAW_TOOL", getKeyboardEventInfo(e)) - break - } - case "t": { + case 'd': { if (metaKey(e)) { - state.send("DUPLICATED", getKeyboardEventInfo(e)) + state.send('DUPLICATED', getKeyboardEventInfo(e)) } else { - state.send("SELECTED_DOT_TOOL", getKeyboardEventInfo(e)) + state.send('SELECTED_DRAW_TOOL', getKeyboardEventInfo(e)) } break } - case "c": { + case 't': { if (metaKey(e)) { - state.send("COPIED", getKeyboardEventInfo(e)) + state.send('DUPLICATED', getKeyboardEventInfo(e)) } else { - state.send("SELECTED_CIRCLE_TOOL", getKeyboardEventInfo(e)) + state.send('SELECTED_DOT_TOOL', getKeyboardEventInfo(e)) } break } - case "i": { + case 'c': { + if (metaKey(e)) { + state.send('COPIED', getKeyboardEventInfo(e)) + } else { + state.send('SELECTED_CIRCLE_TOOL', getKeyboardEventInfo(e)) + } + break + } + case 'i': { if (metaKey(e)) { } else { - state.send("SELECTED_ELLIPSE_TOOL", getKeyboardEventInfo(e)) + state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e)) } break } - case "l": { + case 'l': { if (metaKey(e)) { } else { - state.send("SELECTED_LINE_TOOL", getKeyboardEventInfo(e)) + state.send('SELECTED_LINE_TOOL', getKeyboardEventInfo(e)) } break } - case "y": { + case 'y': { if (metaKey(e)) { } else { - state.send("SELECTED_RAY_TOOL", getKeyboardEventInfo(e)) + state.send('SELECTED_RAY_TOOL', getKeyboardEventInfo(e)) } break } - case "p": { + case 'p': { if (metaKey(e)) { } else { - state.send("SELECTED_POLYLINE_TOOL", getKeyboardEventInfo(e)) + state.send('SELECTED_POLYLINE_TOOL', getKeyboardEventInfo(e)) } break } - case "r": { + case 'r': { if (metaKey(e)) { } else { - state.send("SELECTED_RECTANGLE_TOOL", getKeyboardEventInfo(e)) + state.send('SELECTED_RECTANGLE_TOOL', getKeyboardEventInfo(e)) } break } default: { - state.send("PRESSED_KEY", getKeyboardEventInfo(e)) + state.send('PRESSED_KEY', getKeyboardEventInfo(e)) } } } function handleKeyUp(e: KeyboardEvent) { - if (e.key === "Shift") { - state.send("RELEASED_SHIFT_KEY", getKeyboardEventInfo(e)) + if (e.key === 'Shift') { + state.send('RELEASED_SHIFT_KEY', getKeyboardEventInfo(e)) } - if (e.key === "Alt") { - state.send("RELEASED_ALT_KEY", getKeyboardEventInfo(e)) + if (e.key === 'Alt') { + state.send('RELEASED_ALT_KEY', getKeyboardEventInfo(e)) } - state.send("RELEASED_KEY", getKeyboardEventInfo(e)) + state.send('RELEASED_KEY', getKeyboardEventInfo(e)) } - document.body.addEventListener("keydown", handleKeyDown) - document.body.addEventListener("keyup", handleKeyUp) + document.body.addEventListener('keydown', handleKeyDown) + document.body.addEventListener('keyup', handleKeyUp) return () => { - document.body.removeEventListener("keydown", handleKeyDown) - document.body.removeEventListener("keyup", handleKeyUp) + document.body.removeEventListener('keydown', handleKeyDown) + document.body.removeEventListener('keyup', handleKeyUp) } }, []) } diff --git a/lib/colors.ts b/lib/colors.ts index 357b1e8e8..611a238f4 100644 --- a/lib/colors.ts +++ b/lib/colors.ts @@ -1,38 +1,38 @@ export const shades = { - transparent: "transparent", - white: "rgba(248, 249, 250, 1.000)", - lightGray: "rgba(224, 226, 230, 1.000)", - gray: "rgba(172, 181, 189, 1.000)", - darkGray: "rgba(52, 58, 64, 1.000)", - black: "rgba(0,0,0, 1.000)", + none: 'none', + white: 'rgba(248, 249, 250, 1.000)', + lightGray: 'rgba(224, 226, 230, 1.000)', + gray: 'rgba(172, 181, 189, 1.000)', + darkGray: 'rgba(52, 58, 64, 1.000)', + black: 'rgba(0,0,0, 1.000)', } export const strokes = { - lime: "rgba(115, 184, 23, 1.000)", - green: "rgba(54, 178, 77, 1.000)", - teal: "rgba(9, 167, 120, 1.000)", - cyan: "rgba(14, 152, 173, 1.000)", - blue: "rgba(28, 126, 214, 1.000)", - indigo: "rgba(66, 99, 235, 1.000)", - violet: "rgba(112, 72, 232, 1.000)", - grape: "rgba(174, 62, 200, 1.000)", - pink: "rgba(214, 51, 108, 1.000)", - red: "rgba(240, 63, 63, 1.000)", - orange: "rgba(247, 103, 6, 1.000)", - yellow: "rgba(245, 159, 0, 1.000)", + lime: 'rgba(115, 184, 23, 1.000)', + green: 'rgba(54, 178, 77, 1.000)', + teal: 'rgba(9, 167, 120, 1.000)', + cyan: 'rgba(14, 152, 173, 1.000)', + blue: 'rgba(28, 126, 214, 1.000)', + indigo: 'rgba(66, 99, 235, 1.000)', + violet: 'rgba(112, 72, 232, 1.000)', + grape: 'rgba(174, 62, 200, 1.000)', + pink: 'rgba(214, 51, 108, 1.000)', + red: 'rgba(240, 63, 63, 1.000)', + orange: 'rgba(247, 103, 6, 1.000)', + yellow: 'rgba(245, 159, 0, 1.000)', } export const fills = { - lime: "rgba(217, 245, 162, 1.000)", - green: "rgba(177, 242, 188, 1.000)", - teal: "rgba(149, 242, 215, 1.000)", - cyan: "rgba(153, 233, 242, 1.000)", - blue: "rgba(166, 216, 255, 1.000)", - indigo: "rgba(186, 200, 255, 1.000)", - violet: "rgba(208, 191, 255, 1.000)", - grape: "rgba(237, 190, 250, 1.000)", - pink: "rgba(252, 194, 215, 1.000)", - red: "rgba(255, 201, 201, 1.000)", - orange: "rgba(255, 216, 168, 1.000)", - yellow: "rgba(255, 236, 153, 1.000)", + lime: 'rgba(217, 245, 162, 1.000)', + green: 'rgba(177, 242, 188, 1.000)', + teal: 'rgba(149, 242, 215, 1.000)', + cyan: 'rgba(153, 233, 242, 1.000)', + blue: 'rgba(166, 216, 255, 1.000)', + indigo: 'rgba(186, 200, 255, 1.000)', + violet: 'rgba(208, 191, 255, 1.000)', + grape: 'rgba(237, 190, 250, 1.000)', + pink: 'rgba(252, 194, 215, 1.000)', + red: 'rgba(255, 201, 201, 1.000)', + orange: 'rgba(255, 216, 168, 1.000)', + yellow: 'rgba(255, 236, 153, 1.000)', } diff --git a/lib/shape-utils/circle.tsx b/lib/shape-utils/circle.tsx index 098ff4254..2d6a0979c 100644 --- a/lib/shape-utils/circle.tsx +++ b/lib/shape-utils/circle.tsx @@ -1,11 +1,11 @@ -import { v4 as uuid } from "uuid" -import * as vec from "utils/vec" -import { CircleShape, ShapeType } from "types" -import { registerShapeUtils } from "./index" -import { boundsContained } from "utils/bounds" -import { intersectCircleBounds } from "utils/intersections" -import { pointInCircle } from "utils/hitTests" -import { translateBounds } from "utils/utils" +import { v4 as uuid } from 'uuid' +import * as vec from 'utils/vec' +import { CircleShape, ShapeType } from 'types' +import { registerShapeUtils } from './index' +import { boundsContained } from 'utils/bounds' +import { intersectCircleBounds } from 'utils/intersections' +import { pointInCircle } from 'utils/hitTests' +import { translateBounds } from 'utils/utils' const circle = registerShapeUtils({ boundsCache: new WeakMap([]), @@ -15,22 +15,29 @@ const circle = registerShapeUtils({ id: uuid(), type: ShapeType.Circle, isGenerated: false, - name: "Circle", - parentId: "page0", + name: 'Circle', + parentId: 'page0', childIndex: 0, point: [0, 0], rotation: 0, radius: 1, style: { - fill: "#c6cacb", - stroke: "#000", + fill: '#c6cacb', + stroke: '#000', }, ...props, } }, - render({ id, radius }) { - return + render({ id, radius, style }) { + return ( + + ) }, applyStyles(shape, style) { @@ -92,7 +99,7 @@ const circle = registerShapeUtils({ }, translateTo(shape, point) { - shape.point = point + shape.point = vec.toPrecision(point) return this }, diff --git a/lib/shape-utils/dot.tsx b/lib/shape-utils/dot.tsx index 78f56594a..af9bf7d62 100644 --- a/lib/shape-utils/dot.tsx +++ b/lib/shape-utils/dot.tsx @@ -1,11 +1,11 @@ -import { v4 as uuid } from "uuid" -import * as vec from "utils/vec" -import { DotShape, ShapeType } from "types" -import { registerShapeUtils } from "./index" -import { boundsContained } from "utils/bounds" -import { intersectCircleBounds } from "utils/intersections" -import { DotCircle } from "components/canvas/misc" -import { translateBounds } from "utils/utils" +import { v4 as uuid } from 'uuid' +import * as vec from 'utils/vec' +import { DotShape, ShapeType } from 'types' +import { registerShapeUtils } from './index' +import { boundsContained } from 'utils/bounds' +import { intersectCircleBounds } from 'utils/intersections' +import { DotCircle } from 'components/canvas/misc' +import { translateBounds } from 'utils/utils' const dot = registerShapeUtils({ boundsCache: new WeakMap([]), @@ -15,14 +15,14 @@ const dot = registerShapeUtils({ id: uuid(), type: ShapeType.Dot, isGenerated: false, - name: "Dot", - parentId: "page0", + name: 'Dot', + parentId: 'page0', childIndex: 0, point: [0, 0], rotation: 0, style: { - fill: "#c6cacb", - strokeWidth: "0", + fill: '#c6cacb', + strokeWidth: '0', }, ...props, } @@ -79,7 +79,7 @@ const dot = registerShapeUtils({ }, translateTo(shape, point) { - shape.point = point + shape.point = vec.toPrecision(point) return this }, diff --git a/lib/shape-utils/draw.tsx b/lib/shape-utils/draw.tsx index 10df69ea5..4701f3d0c 100644 --- a/lib/shape-utils/draw.tsx +++ b/lib/shape-utils/draw.tsx @@ -34,13 +34,13 @@ const draw = registerShapeUtils({ strokeLinecap: 'round', strokeLinejoin: 'round', ...props.style, - stroke: 'transparent', + fill: props.style.stroke, }, } }, render(shape) { - const { id, point, points } = shape + const { id, point, points, style } = shape if (!pathCache.has(points)) { if (points.length < 2) { @@ -51,7 +51,12 @@ const draw = registerShapeUtils({ } pathCache.set(points, getSvgPathFromStroke(d)) } else { - pathCache.set(points, getSvgPathFromStroke(getStroke(points))) + pathCache.set( + points, + getSvgPathFromStroke( + getStroke(points, { size: Number(style.strokeWidth) * 2 }) + ) + ) } } @@ -60,6 +65,7 @@ const draw = registerShapeUtils({ applyStyles(shape, style) { Object.assign(shape.style, style) + shape.style.fill = shape.style.stroke return this }, @@ -128,7 +134,7 @@ const draw = registerShapeUtils({ }, translateTo(shape, point) { - shape.point = point + shape.point = vec.toPrecision(point) return this }, diff --git a/lib/shape-utils/ellipse.tsx b/lib/shape-utils/ellipse.tsx index 6e7e57d55..42cd244f8 100644 --- a/lib/shape-utils/ellipse.tsx +++ b/lib/shape-utils/ellipse.tsx @@ -1,16 +1,16 @@ -import { v4 as uuid } from "uuid" -import * as vec from "utils/vec" -import { EllipseShape, ShapeType } from "types" -import { registerShapeUtils } from "./index" -import { boundsContained, getRotatedEllipseBounds } from "utils/bounds" -import { intersectEllipseBounds } from "utils/intersections" -import { pointInEllipse } from "utils/hitTests" +import { v4 as uuid } from 'uuid' +import * as vec from 'utils/vec' +import { EllipseShape, ShapeType } from 'types' +import { registerShapeUtils } from './index' +import { boundsContained, getRotatedEllipseBounds } from 'utils/bounds' +import { intersectEllipseBounds } from 'utils/intersections' +import { pointInEllipse } from 'utils/hitTests' import { getBoundsFromPoints, getRotatedCorners, rotateBounds, translateBounds, -} from "utils/utils" +} from 'utils/utils' const ellipse = registerShapeUtils({ boundsCache: new WeakMap([]), @@ -20,24 +20,30 @@ const ellipse = registerShapeUtils({ id: uuid(), type: ShapeType.Ellipse, isGenerated: false, - name: "Ellipse", - parentId: "page0", + name: 'Ellipse', + parentId: 'page0', childIndex: 0, point: [0, 0], radiusX: 1, radiusY: 1, rotation: 0, style: { - fill: "#c6cacb", - stroke: "#000", + fill: '#c6cacb', + stroke: '#000', }, ...props, } }, - render({ id, radiusX, radiusY }) { + render({ id, radiusX, radiusY, style }) { return ( - + ) }, @@ -110,7 +116,7 @@ const ellipse = registerShapeUtils({ }, translateTo(shape, point) { - shape.point = point + shape.point = vec.toPrecision(point) return this }, diff --git a/lib/shape-utils/line.tsx b/lib/shape-utils/line.tsx index dfe7c80d2..14f05a200 100644 --- a/lib/shape-utils/line.tsx +++ b/lib/shape-utils/line.tsx @@ -1,11 +1,11 @@ -import { v4 as uuid } from "uuid" -import * as vec from "utils/vec" -import { LineShape, ShapeType } from "types" -import { registerShapeUtils } from "./index" -import { boundsContained } from "utils/bounds" -import { intersectCircleBounds } from "utils/intersections" -import { DotCircle } from "components/canvas/misc" -import { translateBounds } from "utils/utils" +import { v4 as uuid } from 'uuid' +import * as vec from 'utils/vec' +import { LineShape, ShapeType } from 'types' +import { registerShapeUtils } from './index' +import { boundsContained } from 'utils/bounds' +import { intersectCircleBounds } from 'utils/intersections' +import { DotCircle } from 'components/canvas/misc' +import { translateBounds } from 'utils/utils' const line = registerShapeUtils({ boundsCache: new WeakMap([]), @@ -15,15 +15,15 @@ const line = registerShapeUtils({ id: uuid(), type: ShapeType.Line, isGenerated: false, - name: "Line", - parentId: "page0", + name: 'Line', + parentId: 'page0', childIndex: 0, point: [0, 0], direction: [0, 0], rotation: 0, style: { - fill: "#c6cacb", - stroke: "#000", + fill: '#c6cacb', + stroke: '#000', }, ...props, } @@ -88,7 +88,7 @@ const line = registerShapeUtils({ }, translateTo(shape, point) { - shape.point = point + shape.point = vec.toPrecision(point) return this }, diff --git a/lib/shape-utils/polyline.tsx b/lib/shape-utils/polyline.tsx index e62140d94..500aab30d 100644 --- a/lib/shape-utils/polyline.tsx +++ b/lib/shape-utils/polyline.tsx @@ -1,10 +1,10 @@ -import { v4 as uuid } from "uuid" -import * as vec from "utils/vec" -import { PolylineShape, ShapeType } from "types" -import { registerShapeUtils } from "./index" -import { intersectPolylineBounds } from "utils/intersections" -import { boundsContainPolygon } from "utils/bounds" -import { getBoundsFromPoints, translateBounds } from "utils/utils" +import { v4 as uuid } from 'uuid' +import * as vec from 'utils/vec' +import { PolylineShape, ShapeType } from 'types' +import { registerShapeUtils } from './index' +import { intersectPolylineBounds } from 'utils/intersections' +import { boundsContainPolygon } from 'utils/bounds' +import { getBoundsFromPoints, translateBounds } from 'utils/utils' const polyline = registerShapeUtils({ boundsCache: new WeakMap([]), @@ -14,16 +14,16 @@ const polyline = registerShapeUtils({ id: uuid(), type: ShapeType.Polyline, isGenerated: false, - name: "Polyline", - parentId: "page0", + name: 'Polyline', + parentId: 'page0', childIndex: 0, point: [0, 0], points: [[0, 0]], rotation: 0, style: { strokeWidth: 2, - strokeLinecap: "round", - strokeLinejoin: "round", + strokeLinecap: 'round', + strokeLinejoin: 'round', }, ...props, } @@ -97,7 +97,7 @@ const polyline = registerShapeUtils({ }, translateTo(shape, point) { - shape.point = point + shape.point = vec.toPrecision(point) return this }, diff --git a/lib/shape-utils/ray.tsx b/lib/shape-utils/ray.tsx index 750eb648f..823971b34 100644 --- a/lib/shape-utils/ray.tsx +++ b/lib/shape-utils/ray.tsx @@ -1,11 +1,11 @@ -import { v4 as uuid } from "uuid" -import * as vec from "utils/vec" -import { RayShape, ShapeType } from "types" -import { registerShapeUtils } from "./index" -import { boundsContained } from "utils/bounds" -import { intersectCircleBounds } from "utils/intersections" -import { DotCircle } from "components/canvas/misc" -import { translateBounds } from "utils/utils" +import { v4 as uuid } from 'uuid' +import * as vec from 'utils/vec' +import { RayShape, ShapeType } from 'types' +import { registerShapeUtils } from './index' +import { boundsContained } from 'utils/bounds' +import { intersectCircleBounds } from 'utils/intersections' +import { DotCircle } from 'components/canvas/misc' +import { translateBounds } from 'utils/utils' const ray = registerShapeUtils({ boundsCache: new WeakMap([]), @@ -15,15 +15,15 @@ const ray = registerShapeUtils({ id: uuid(), type: ShapeType.Ray, isGenerated: false, - name: "Ray", - parentId: "page0", + name: 'Ray', + parentId: 'page0', childIndex: 0, point: [0, 0], direction: [0, 1], rotation: 0, style: { - fill: "#c6cacb", - stroke: "#000", + fill: '#c6cacb', + stroke: '#000', strokeWidth: 1, }, ...props, @@ -88,7 +88,7 @@ const ray = registerShapeUtils({ }, translateTo(shape, point) { - shape.point = point + shape.point = vec.toPrecision(point) return this }, diff --git a/lib/shape-utils/rectangle.tsx b/lib/shape-utils/rectangle.tsx index a0c98f313..9619c08b5 100644 --- a/lib/shape-utils/rectangle.tsx +++ b/lib/shape-utils/rectangle.tsx @@ -1,13 +1,13 @@ -import { v4 as uuid } from "uuid" -import * as vec from "utils/vec" -import { RectangleShape, ShapeType } from "types" -import { registerShapeUtils } from "./index" -import { boundsCollidePolygon, boundsContainPolygon } from "utils/bounds" +import { v4 as uuid } from 'uuid' +import * as vec from 'utils/vec' +import { RectangleShape, ShapeType } from 'types' +import { registerShapeUtils } from './index' +import { boundsCollidePolygon, boundsContainPolygon } from 'utils/bounds' import { getBoundsFromPoints, getRotatedCorners, translateBounds, -} from "utils/utils" +} from 'utils/utils' const rectangle = registerShapeUtils({ boundsCache: new WeakMap([]), @@ -17,42 +17,31 @@ const rectangle = registerShapeUtils({ id: uuid(), type: ShapeType.Rectangle, isGenerated: false, - name: "Rectangle", - parentId: "page0", + name: 'Rectangle', + parentId: 'page0', childIndex: 0, point: [0, 0], size: [1, 1], radius: 2, rotation: 0, style: { - fill: "#c6cacb", - stroke: "#000", + fill: '#c6cacb', + stroke: '#000', }, ...props, } }, - render({ id, size, radius, childIndex }) { + render({ id, size, radius, style }) { return ( - - {childIndex} - ) }, @@ -113,7 +102,7 @@ const rectangle = registerShapeUtils({ }, translateTo(shape, point) { - shape.point = point + shape.point = vec.toPrecision(point) return this }, diff --git a/package.json b/package.json index ddcc1944a..9bc4aa916 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "@monaco-editor/react": "^4.1.3", "@radix-ui/react-dropdown-menu": "^0.0.19", "@radix-ui/react-icons": "^1.0.3", + "@radix-ui/react-radio-group": "^0.0.16", "@state-designer/react": "^1.7.1", "@stitches/react": "^0.1.9", "framer-motion": "^4.1.16", diff --git a/state/commands/duplicate.ts b/state/commands/duplicate.ts new file mode 100644 index 000000000..5213dffe2 --- /dev/null +++ b/state/commands/duplicate.ts @@ -0,0 +1,48 @@ +import Command from './command' +import history from '../history' +import { Data } from 'types' +import { getPage, getSelectedShapes } from 'utils/utils' +import { v4 as uuid } from 'uuid' +import { current } from 'immer' +import * as vec from 'utils/vec' + +export default function duplicateCommand(data: Data) { + const { currentPageId } = data + const selectedShapes = getSelectedShapes(current(data)) + const duplicates = selectedShapes.map((shape) => ({ + ...shape, + id: uuid(), + point: vec.add(shape.point, vec.div([16, 16], data.camera.zoom)), + })) + + history.execute( + data, + new Command({ + name: 'duplicate_shapes', + category: 'canvas', + manualSelection: true, + do(data) { + const { shapes } = getPage(data, currentPageId) + + data.selectedIds.clear() + + for (const duplicate of duplicates) { + shapes[duplicate.id] = duplicate + data.selectedIds.add(duplicate.id) + } + }, + undo(data) { + const { shapes } = getPage(data, currentPageId) + data.selectedIds.clear() + + for (const duplicate of duplicates) { + delete shapes[duplicate.id] + } + + for (let id in selectedShapes) { + data.selectedIds.add(id) + } + }, + }) + ) +} diff --git a/state/commands/index.ts b/state/commands/index.ts index f82d23d4a..8e395eafe 100644 --- a/state/commands/index.ts +++ b/state/commands/index.ts @@ -1,22 +1,25 @@ -import align from "./align" -import deleteSelected from "./delete-selected" -import direct from "./direct" -import distribute from "./distribute" -import generate from "./generate" -import move from "./move" -import draw from "./draw" -import rotate from "./rotate" -import stretch from "./stretch" -import style from "./style" -import transform from "./transform" -import transformSingle from "./transform-single" -import translate from "./translate" +import align from './align' +import deleteSelected from './delete-selected' +import direct from './direct' +import distribute from './distribute' +import duplicate from './duplicate' +import generate from './generate' +import move from './move' +import draw from './draw' +import rotate from './rotate' +import stretch from './stretch' +import style from './style' +import transform from './transform' +import transformSingle from './transform-single' +import translate from './translate' +import nudge from './nudge' const commands = { align, deleteSelected, direct, distribute, + duplicate, generate, move, draw, @@ -26,6 +29,7 @@ const commands = { transform, transformSingle, translate, + nudge, } export default commands diff --git a/state/commands/nudge.ts b/state/commands/nudge.ts new file mode 100644 index 000000000..aea6feab3 --- /dev/null +++ b/state/commands/nudge.ts @@ -0,0 +1,40 @@ +import Command from './command' +import history from '../history' +import { Data } from 'types' +import { getPage, getSelectedShapes } from 'utils/utils' +import { getShapeUtils } from 'lib/shape-utils' +import * as vec from 'utils/vec' + +export default function nudgeCommand(data: Data, delta: number[]) { + const { currentPageId } = data + const selectedShapes = getSelectedShapes(data) + const shapeBounds = Object.fromEntries( + selectedShapes.map( + (shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const + ) + ) + + history.execute( + data, + new Command({ + name: 'set_direction', + category: 'canvas', + do(data) { + const { shapes } = getPage(data, currentPageId) + + for (let id in shapeBounds) { + const shape = shapes[id] + getShapeUtils(shape).translateTo(shape, vec.add(shape.point, delta)) + } + }, + undo(data) { + const { shapes } = getPage(data, currentPageId) + + for (let id in shapeBounds) { + const shape = shapes[id] + getShapeUtils(shape).translateTo(shape, vec.sub(shape.point, delta)) + } + }, + }) + ) +} diff --git a/state/commands/transform-single.ts b/state/commands/transform-single.ts index 43ae56868..198f30ee6 100644 --- a/state/commands/transform-single.ts +++ b/state/commands/transform-single.ts @@ -1,10 +1,10 @@ -import Command from "./command" -import history from "../history" -import { Data, Corner, Edge } from "types" -import { getShapeUtils } from "lib/shape-utils" -import { current } from "immer" -import { TransformSingleSnapshot } from "state/sessions/transform-single-session" -import { getPage } from "utils/utils" +import Command from './command' +import history from '../history' +import { Data, Corner, Edge } from 'types' +import { getShapeUtils } from 'lib/shape-utils' +import { current } from 'immer' +import { TransformSingleSnapshot } from 'state/sessions/transform-single-session' +import { getPage } from 'utils/utils' export default function transformSingleCommand( data: Data, @@ -14,13 +14,13 @@ export default function transformSingleCommand( scaleY: number, isCreating: boolean ) { - const shape = getPage(data, after.currentPageId).shapes[after.id] + const shape = current(getPage(data, after.currentPageId).shapes[after.id]) history.execute( data, new Command({ - name: "transform_single_shape", - category: "canvas", + name: 'transform_single_shape', + category: 'canvas', manualSelection: true, do(data) { const { id, type, initialShape, initialShapeBounds } = after diff --git a/state/state.ts b/state/state.ts index f35028ab1..6a5b60e41 100644 --- a/state/state.ts +++ b/state/state.ts @@ -41,10 +41,15 @@ const initialData: Data = { isDarkMode: false, isCodeOpen: false, isStyleOpen: false, + isToolLocked: false, + isPenLocked: false, + nudgeDistanceLarge: 10, + nudgeDistanceSmall: 1, }, currentStyle: { fill: shades.lightGray, stroke: shades.darkGray, + strokeWidth: 2, }, camera: { point: [0, 0], @@ -94,6 +99,9 @@ const state = createState({ else: 'zoomCameraToActual', }, SELECTED_ALL: { to: 'selecting', do: 'selectAll' }, + NUDGED: { do: 'nudgeSelection' }, + USED_PEN_DEVICE: 'enablePenLock', + DISABLED_PEN_LOCK: 'disablePenLock', }, initial: 'loading', states: { @@ -124,20 +132,22 @@ const state = createState({ selecting: { on: { SAVED: 'forceSave', - UNDO: { do: 'undo' }, - REDO: { do: 'redo' }, - CANCELLED: { do: 'clearSelectedIds' }, - DELETED: { do: 'deleteSelectedIds' }, + UNDO: 'undo', + REDO: 'redo', SAVED_CODE: 'saveCode', - GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'], + CANCELLED: 'clearSelectedIds', + DELETED: 'deleteSelectedIds', + STARTED_PINCHING: { to: 'pinching' }, INCREASED_CODE_FONT_SIZE: 'increaseCodeFontSize', DECREASED_CODE_FONT_SIZE: 'decreaseCodeFontSize', CHANGED_CODE_CONTROL: 'updateControls', - ALIGNED: 'alignSelection', - STRETCHED: 'stretchSelection', - DISTRIBUTED: 'distributeSelection', - MOVED: 'moveSelection', - STARTED_PINCHING: { to: 'pinching' }, + GENERATED_FROM_CODE: ['setCodeControls', 'setGeneratedShapes'], + TOGGLED_TOOL_LOCK: 'toggleToolLock', + MOVED: { if: 'hasSelection', do: 'moveSelection' }, + ALIGNED: { if: 'hasSelection', do: 'alignSelection' }, + STRETCHED: { if: 'hasSelection', do: 'stretchSelection' }, + DISTRIBUTED: { if: 'hasSelection', do: 'distributeSelection' }, + DUPLICATED: { if: 'hasSelection', do: 'duplicateSelection' }, }, initial: 'notPointing', states: { @@ -262,239 +272,257 @@ const state = createState({ PINCHED: { do: 'pinchCamera' }, }, }, - draw: { - initial: 'creating', + usingTool: { + initial: 'draw', states: { - creating: { - on: { - CANCELLED: { to: 'selecting' }, - POINTED_CANVAS: { - get: 'newDraw', - do: 'createShape', - to: 'draw.editing', - }, - UNDO: { do: 'undo' }, - REDO: { do: 'redo' }, - }, - }, - editing: { - onEnter: 'startDrawSession', - on: { - STOPPED_POINTING: { - do: 'completeSession', - to: 'draw.creating', - }, - CANCELLED: { - do: ['cancelSession', 'deleteSelectedIds'], - to: 'selecting', - }, - MOVED_POINTER: 'updateDrawSession', - PANNED_CAMERA: 'updateDrawSession', - }, - }, - }, - }, - dot: { - initial: 'creating', - states: { - creating: { - on: { - CANCELLED: { to: 'selecting' }, - POINTED_CANVAS: { - get: 'newDot', - do: 'createShape', - to: 'dot.editing', - }, - }, - }, - editing: { - on: { - STOPPED_POINTING: { do: 'completeSession', to: 'selecting' }, - CANCELLED: { - do: ['cancelSession', 'deleteSelectedIds'], - to: 'selecting', - }, - }, - initial: 'inactive', + draw: { + initial: 'creating', states: { - inactive: { + creating: { on: { - MOVED_POINTER: { - if: 'distanceImpliesDrag', - to: 'dot.editing.active', + CANCELLED: { to: 'selecting' }, + POINTED_CANVAS: { + get: 'newDraw', + do: 'createShape', + to: 'draw.editing', + }, + UNDO: { do: 'undo' }, + REDO: { do: 'redo' }, + }, + }, + editing: { + onEnter: 'startDrawSession', + on: { + STOPPED_POINTING: { + do: 'completeSession', + to: 'draw.creating', + }, + CANCELLED: { + do: ['cancelSession', 'deleteSelectedIds'], + to: 'selecting', + }, + MOVED_POINTER: 'updateDrawSession', + PANNED_CAMERA: 'updateDrawSession', + }, + }, + }, + }, + dot: { + initial: 'creating', + states: { + creating: { + on: { + CANCELLED: { to: 'selecting' }, + POINTED_CANVAS: { + get: 'newDot', + do: 'createShape', + to: 'dot.editing', }, }, }, - active: { - onEnter: 'startTranslateSession', + editing: { on: { - MOVED_POINTER: 'updateTranslateSession', - PANNED_CAMERA: 'updateTranslateSession', + STOPPED_POINTING: [ + 'completeSession', + { + if: 'isToolLocked', + to: 'dot.creating', + else: { + to: 'selecting', + }, + }, + ], + CANCELLED: { + do: ['cancelSession', 'deleteSelectedIds'], + to: 'selecting', + }, + }, + initial: 'inactive', + states: { + inactive: { + on: { + MOVED_POINTER: { + if: 'distanceImpliesDrag', + to: 'dot.editing.active', + }, + }, + }, + active: { + onEnter: 'startTranslateSession', + on: { + MOVED_POINTER: 'updateTranslateSession', + PANNED_CAMERA: 'updateTranslateSession', + }, + }, }, }, }, }, - }, - }, - circle: { - initial: 'creating', - states: { - creating: { - on: { - CANCELLED: { to: 'selecting' }, - POINTED_CANVAS: { - to: 'circle.editing', + circle: { + initial: 'creating', + states: { + creating: { + on: { + CANCELLED: { to: 'selecting' }, + POINTED_CANVAS: { + to: 'circle.editing', + }, + }, }, - }, - }, - editing: { - on: { - STOPPED_POINTING: { to: 'selecting' }, - CANCELLED: { to: 'selecting' }, - MOVED_POINTER: { - if: 'distanceImpliesDrag', - then: { - get: 'newCircle', - do: 'createShape', - to: 'drawingShape.bounds', + editing: { + on: { + STOPPED_POINTING: { to: 'selecting' }, + CANCELLED: { to: 'selecting' }, + MOVED_POINTER: { + if: 'distanceImpliesDrag', + then: { + get: 'newCircle', + do: 'createShape', + to: 'drawingShape.bounds', + }, + }, }, }, }, }, - }, - }, - ellipse: { - initial: 'creating', - states: { - creating: { - on: { - CANCELLED: { to: 'selecting' }, - POINTED_CANVAS: { - to: 'ellipse.editing', + ellipse: { + initial: 'creating', + states: { + creating: { + on: { + CANCELLED: { to: 'selecting' }, + POINTED_CANVAS: { + to: 'ellipse.editing', + }, + }, }, - }, - }, - editing: { - on: { - STOPPED_POINTING: { to: 'selecting' }, - CANCELLED: { to: 'selecting' }, - MOVED_POINTER: { - if: 'distanceImpliesDrag', - then: { - get: 'newEllipse', - do: 'createShape', - to: 'drawingShape.bounds', + editing: { + on: { + STOPPED_POINTING: { to: 'selecting' }, + CANCELLED: { to: 'selecting' }, + MOVED_POINTER: { + if: 'distanceImpliesDrag', + then: { + get: 'newEllipse', + do: 'createShape', + to: 'drawingShape.bounds', + }, + }, }, }, }, }, - }, - }, - rectangle: { - initial: 'creating', - states: { - creating: { - on: { - CANCELLED: { to: 'selecting' }, - POINTED_CANVAS: { - to: 'rectangle.editing', + rectangle: { + initial: 'creating', + states: { + creating: { + on: { + CANCELLED: { to: 'selecting' }, + POINTED_CANVAS: { + to: 'rectangle.editing', + }, + }, }, - }, - }, - editing: { - on: { - STOPPED_POINTING: { to: 'selecting' }, - CANCELLED: { to: 'selecting' }, - MOVED_POINTER: { - if: 'distanceImpliesDrag', - then: { - get: 'newRectangle', - do: 'createShape', - to: 'drawingShape.bounds', + editing: { + on: { + STOPPED_POINTING: { to: 'selecting' }, + CANCELLED: { to: 'selecting' }, + MOVED_POINTER: { + if: 'distanceImpliesDrag', + then: { + get: 'newRectangle', + do: 'createShape', + to: 'drawingShape.bounds', + }, + }, }, }, }, }, + ray: { + initial: 'creating', + states: { + creating: { + on: { + CANCELLED: { to: 'selecting' }, + POINTED_CANVAS: { + get: 'newRay', + do: 'createShape', + to: 'ray.editing', + }, + }, + }, + editing: { + on: { + STOPPED_POINTING: { to: 'selecting' }, + CANCELLED: { to: 'selecting' }, + MOVED_POINTER: { + if: 'distanceImpliesDrag', + to: 'drawingShape.direction', + }, + }, + }, + }, + }, + line: { + initial: 'creating', + states: { + creating: { + on: { + CANCELLED: { to: 'selecting' }, + POINTED_CANVAS: { + get: 'newLine', + do: 'createShape', + to: 'line.editing', + }, + }, + }, + editing: { + on: { + STOPPED_POINTING: { to: 'selecting' }, + CANCELLED: { to: 'selecting' }, + MOVED_POINTER: { + if: 'distanceImpliesDrag', + to: 'drawingShape.direction', + }, + }, + }, + }, + }, + polyline: {}, }, }, - ray: { - initial: 'creating', - states: { - creating: { - on: { - CANCELLED: { to: 'selecting' }, - POINTED_CANVAS: { - get: 'newRay', - do: 'createShape', - to: 'ray.editing', - }, - }, - }, - editing: { - on: { - STOPPED_POINTING: { to: 'selecting' }, - CANCELLED: { to: 'selecting' }, - MOVED_POINTER: { - if: 'distanceImpliesDrag', - to: 'drawingShape.direction', - }, - }, - }, - }, - }, - line: { - initial: 'creating', - states: { - creating: { - on: { - CANCELLED: { to: 'selecting' }, - POINTED_CANVAS: { - get: 'newLine', - do: 'createShape', - to: 'line.editing', - }, - }, - }, - editing: { - on: { - STOPPED_POINTING: { to: 'selecting' }, - CANCELLED: { to: 'selecting' }, - MOVED_POINTER: { - if: 'distanceImpliesDrag', - to: 'drawingShape.direction', - }, - }, - }, - }, - }, - polyline: {}, - }, - }, - drawingShape: { - on: { - STOPPED_POINTING: { - do: 'completeSession', - to: 'selecting', - }, - CANCELLED: { - do: ['cancelSession', 'deleteSelectedIds'], - to: 'selecting', - }, - }, - initial: 'drawingShapeBounds', - states: { - bounds: { - onEnter: 'startDrawTransformSession', + drawingShape: { on: { - MOVED_POINTER: 'updateTransformSession', - PANNED_CAMERA: 'updateTransformSession', + STOPPED_POINTING: [ + 'completeSession', + { + if: 'isToolLocked', + to: 'usingTool.previous', + else: { to: 'selecting' }, + }, + ], + CANCELLED: { + do: ['cancelSession', 'deleteSelectedIds'], + to: 'selecting', + }, }, - }, - direction: { - onEnter: 'startDirectionSession', - on: { - MOVED_POINTER: 'updateDirectionSession', - PANNED_CAMERA: 'updateDirectionSession', + initial: 'drawingShapeBounds', + states: { + bounds: { + onEnter: 'startDrawTransformSession', + on: { + MOVED_POINTER: 'updateTransformSession', + PANNED_CAMERA: 'updateTransformSession', + }, + }, + direction: { + onEnter: 'startDirectionSession', + on: { + MOVED_POINTER: 'updateDirectionSession', + PANNED_CAMERA: 'updateDirectionSession', + }, + }, }, }, }, @@ -562,6 +590,12 @@ const state = createState({ hasSelection(data) { return data.selectedIds.size > 0 }, + isToolLocked(data) { + return data.settings.isToolLocked + }, + isPenLocked(data) { + return data.settings.isPenLocked + }, }, actions: { /* --------------------- Shapes --------------------- */ @@ -712,6 +746,19 @@ const state = createState({ session.update(data, screenToWorld(payload.point, data)) }, + // Nudges + nudgeSelection(data, payload: { delta: number[]; shiftKey: boolean }) { + commands.nudge( + data, + vec.mul( + payload.delta, + payload.shiftKey + ? data.settings.nudgeDistanceLarge + : data.settings.nudgeDistanceSmall + ) + ) + }, + /* -------------------- Selection ------------------- */ selectAll(data) { @@ -756,6 +803,9 @@ const state = createState({ distributeSelection(data, payload: { type: DistributeType }) { commands.distribute(data, payload.type) }, + duplicateSelection(data) { + commands.duplicate(data) + }, /* --------------------- Camera --------------------- */ @@ -913,6 +963,7 @@ const state = createState({ }, /* ---------------------- Code ---------------------- */ + closeCodePanel(data) { data.settings.isCodeOpen = false }, @@ -962,7 +1013,20 @@ const state = createState({ history.enable() }, - // Data + /* -------------------- Settings -------------------- */ + + enablePenLock(data) { + data.settings.isPenLocked = true + }, + disablePenLock(data) { + data.settings.isPenLocked = false + }, + toggleToolLock(data) { + data.settings.isToolLocked = !data.settings.isToolLocked + }, + + /* ---------------------- Data ---------------------- */ + saveCode(data, payload: { code: string }) { data.document.code[data.currentCodeFileId].code = payload.code history.save(data) diff --git a/styles/stitches.config.ts b/styles/stitches.config.ts index f8db4a406..b573e91f9 100644 --- a/styles/stitches.config.ts +++ b/styles/stitches.config.ts @@ -1,4 +1,4 @@ -import { createCss, defaultThemeMap } from "@stitches/react" +import { createCss, defaultThemeMap } from '@stitches/react' const { styled, global, css, theme, getCssString } = createCss({ themeMap: { @@ -6,26 +6,27 @@ const { styled, global, css, theme, getCssString } = createCss({ }, theme: { colors: { - brushFill: "rgba(0,0,0,.1)", - 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)", - border: "#aaa", - panel: "#fefefe", - hover: "#efefef", - text: "#333", - input: "#f3f3f3", - inputBorder: "#ddd", + brushFill: 'rgba(0,0,0,.1)', + 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)', + border: '#aaa', + panel: '#fefefe', + inactive: '#cccccf', + hover: '#efefef', + text: '#333', + input: '#f3f3f3', + inputBorder: '#ddd', }, space: {}, fontSizes: { - 0: "10px", - 1: "12px", - 2: "13px", - 3: "16px", - 4: "18px", + 0: '10px', + 1: '12px', + 2: '13px', + 3: '16px', + 4: '18px', }, fonts: { ui: '"Recursive", system-ui, sans-serif', @@ -72,17 +73,17 @@ const light = theme({}) const dark = theme({}) const globalStyles = global({ - "*": { boxSizing: "border-box" }, - ":root": { - "--camera-zoom": 1, - "--scale": "calc(1 / var(--camera-zoom))", + '*': { boxSizing: 'border-box' }, + ':root': { + '--camera-zoom': 1, + '--scale': 'calc(1 / var(--camera-zoom))', }, - "html, body": { - padding: "0px", - margin: "0px", - overscrollBehavior: "none", - fontFamily: "$ui", - fontSize: "$2", + 'html, body': { + padding: '0px', + margin: '0px', + overscrollBehavior: 'none', + fontFamily: '$ui', + fontSize: '$2', }, }) diff --git a/types.ts b/types.ts index 5b7ab9973..b137d7163 100644 --- a/types.ts +++ b/types.ts @@ -1,6 +1,6 @@ -import * as monaco from "monaco-editor/esm/vs/editor/editor.api" +import * as monaco from 'monaco-editor/esm/vs/editor/editor.api' -import React from "react" +import React from 'react' /* -------------------------------------------------- */ /* Client State */ @@ -13,6 +13,10 @@ export interface Data { isDarkMode: boolean isCodeOpen: boolean isStyleOpen: boolean + nudgeDistanceSmall: number + nudgeDistanceLarge: number + isToolLocked: boolean + isPenLocked: boolean } currentStyle: ShapeStyles camera: { @@ -39,21 +43,21 @@ export interface Data { export interface Page { id: string - type: "page" + type: 'page' childIndex: number name: string shapes: Record } export enum ShapeType { - Dot = "dot", - Circle = "circle", - Ellipse = "ellipse", - Line = "line", - Ray = "ray", - Polyline = "polyline", - Rectangle = "rectangle", - Draw = "draw", + Dot = 'dot', + Circle = 'circle', + Ellipse = 'ellipse', + Line = 'line', + Ray = 'ray', + Polyline = 'polyline', + Rectangle = 'rectangle', + Draw = 'draw', } // Consider: @@ -164,17 +168,17 @@ export interface PointerInfo { } export enum Edge { - Top = "top_edge", - Right = "right_edge", - Bottom = "bottom_edge", - Left = "left_edge", + Top = 'top_edge', + Right = 'right_edge', + Bottom = 'bottom_edge', + Left = 'left_edge', } export enum Corner { - TopLeft = "top_left_corner", - TopRight = "top_right_corner", - BottomRight = "bottom_right_corner", - BottomLeft = "bottom_left_corner", + TopLeft = 'top_left_corner', + TopRight = 'top_right_corner', + BottomRight = 'bottom_right_corner', + BottomLeft = 'bottom_left_corner', } export interface Bounds { @@ -262,10 +266,10 @@ export type IMonaco = typeof monaco export type IMonacoEditor = monaco.editor.IStandaloneCodeEditor export enum ControlType { - Number = "number", - Vector = "vector", - Text = "text", - Select = "select", + Number = 'number', + Vector = 'vector', + Text = 'text', + Select = 'select', } export interface BaseCodeControl { @@ -296,7 +300,7 @@ export interface TextCodeControl extends BaseCodeControl { format?: (value: string) => string } -export interface SelectCodeControl +export interface SelectCodeControl extends BaseCodeControl { type: ControlType.Select value: T diff --git a/utils/utils.ts b/utils/utils.ts index a106bfcdf..08a86450a 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -1,9 +1,9 @@ -import Vector from "lib/code/vector" -import React from "react" -import { Data, Bounds, Edge, Corner, Shape, ShapeStyles } from "types" -import * as vec from "./vec" -import _isMobile from "ismobilejs" -import { getShapeUtils } from "lib/shape-utils" +import Vector from 'lib/code/vector' +import React from 'react' +import { Data, Bounds, Edge, Corner, Shape, ShapeStyles } from 'types' +import * as vec from './vec' +import _isMobile from 'ismobilejs' +import { getShapeUtils } from 'lib/shape-utils' export function screenToWorld(point: number[], data: Data) { return vec.sub(vec.div(point, data.camera.zoom), data.camera.point) @@ -132,7 +132,7 @@ export function getBezierCurveSegments(points: number[][], tension = 0.4) { cpoints: number[][] = [...points] if (len < 2) { - throw Error("Curve must have at least two points.") + throw Error('Curve must have at least two points.') } for (let i = 1; i < len - 1; i++) { @@ -260,12 +260,12 @@ export function copyToClipboard(string: string) { navigator.clipboard.writeText(string) } catch (e) { try { - textarea = document.createElement("textarea") - textarea.setAttribute("position", "fixed") - textarea.setAttribute("top", "0") - textarea.setAttribute("readonly", "true") - textarea.setAttribute("contenteditable", "true") - textarea.style.position = "fixed" // prevent scroll from jumping to the bottom when focus is set. + textarea = document.createElement('textarea') + textarea.setAttribute('position', 'fixed') + textarea.setAttribute('top', '0') + textarea.setAttribute('readonly', 'true') + textarea.setAttribute('contenteditable', 'true') + textarea.style.position = 'fixed' // prevent scroll from jumping to the bottom when focus is set. textarea.value = string document.body.appendChild(textarea) @@ -281,7 +281,7 @@ export function copyToClipboard(string: string) { sel.addRange(range) textarea.setSelectionRange(0, textarea.value.length) - result = document.execCommand("copy") + result = document.execCommand('copy') } catch (err) { result = null } finally { @@ -549,7 +549,7 @@ export function arrsIntersect( export function getTouchDisplay() { return ( - "ontouchstart" in window || + 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 ) @@ -604,7 +604,7 @@ export function modulate( 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 { - return Math.max(min, typeof max !== "undefined" ? Math.min(n, max) : n) + return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n) } // CURVES @@ -871,8 +871,8 @@ export async function postJsonToEndpoint( const d = await fetch( `${process.env.NEXT_PUBLIC_BASE_API_URL}/api/${endpoint}`, { - method: "POST", - headers: { "Content-Type": "application/json" }, + method: 'POST', + headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), } ) @@ -962,7 +962,7 @@ export function getTransformAnchor( } export function vectorToPoint(point: number[] | Vector | undefined) { - if (typeof point === "undefined") { + if (typeof point === 'undefined') { return [0, 0] } @@ -1062,7 +1062,7 @@ export function getRotatedCorners(b: Bounds, rotation: number) { export function getTransformedBoundingBox( bounds: Bounds, - handle: Corner | Edge | "center", + handle: Corner | Edge | 'center', delta: number[], rotation = 0, isAspectRatioLocked = false @@ -1076,7 +1076,7 @@ export function getTransformedBoundingBox( let [bx1, by1] = [bounds.maxX, bounds.maxY] // If the drag is on the center, just translate the bounds. - if (handle === "center") { + if (handle === 'center') { return { minX: bx0 + delta[0], minY: by0 + delta[1], @@ -1491,7 +1491,7 @@ export function forceIntegerChildIndices(shapes: Shape[]) { } } export function setZoomCSS(zoom: number) { - document.documentElement.style.setProperty("--camera-zoom", zoom.toString()) + document.documentElement.style.setProperty('--camera-zoom', zoom.toString()) } export function getCurrent(source: T): T { @@ -1539,7 +1539,7 @@ export function simplify(points: number[][], tolerance = 1) { } export function getSvgPathFromStroke(stroke: number[][]) { - if (!stroke.length) return "" + if (!stroke.length) return '' const d = stroke.reduce( (acc, [x0, y0], i, arr) => { @@ -1547,9 +1547,9 @@ export function getSvgPathFromStroke(stroke: number[][]) { acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2) return acc }, - ["M", ...stroke[0], "Q"] + ['M', ...stroke[0], 'Q'] ) - d.push("Z") - return d.join(" ") + d.push('Z') + return d.join(' ') } diff --git a/utils/vec.ts b/utils/vec.ts index 8c32fd15a..3b3d72b03 100644 --- a/utils/vec.ts +++ b/utils/vec.ts @@ -483,6 +483,6 @@ export function nudge(A: number[], B: number[], d: number) { * @param a * @param n */ -export function toPrecision(a: number[], n = 3) { +export function toPrecision(a: number[], n = 4) { return [+a[0].toPrecision(n), +a[1].toPrecision(n)] } diff --git a/yarn.lock b/yarn.lock index 9443e0838..66a52a5be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1358,6 +1358,17 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-label@0.0.13": + version "0.0.13" + resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-0.0.13.tgz#b71930fa16a2cf859296317436cb88e31efb8ecf" + integrity sha512-csNElm8qA38pOHr772CXIvBXd/eCGaoAMImuLdawUxQNzwxQ4npd8lr/f9fi/4OLkgeNOVOqjsaVamiNmF/lIw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "0.0.5" + "@radix-ui/react-id" "0.0.6" + "@radix-ui/react-polymorphic" "0.0.11" + "@radix-ui/react-primitive" "0.0.13" + "@radix-ui/react-menu@0.0.18": version "0.0.18" resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-0.0.18.tgz#b36f7657eb6757c623ffc688c48a4781ffd82351" @@ -1431,6 +1442,24 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-polymorphic" "0.0.11" +"@radix-ui/react-radio-group@^0.0.16": + version "0.0.16" + resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-0.0.16.tgz#10fc6e5c3102599cf422e9f6f8d2766088e602a1" + integrity sha512-vOtgflNWcauSul+EvnPCxATdmPw7fb1cuqBJX07yJdjbrw1Iv5v/+d79fNyIwPR+KrkhP+uCMIBfF0gvo6K7ZQ== + 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-label" "0.0.13" + "@radix-ui/react-polymorphic" "0.0.11" + "@radix-ui/react-presence" "0.0.14" + "@radix-ui/react-primitive" "0.0.13" + "@radix-ui/react-roving-focus" "0.0.13" + "@radix-ui/react-slot" "0.0.10" + "@radix-ui/react-use-callback-ref" "0.0.5" + "@radix-ui/react-use-controllable-state" "0.0.6" + "@radix-ui/react-roving-focus@0.0.13": version "0.0.13" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-0.0.13.tgz#c72f503832577979c4caa9efcfd59140730c2f80"