diff --git a/__tests__/children.test.ts b/__tests__/children.test.ts index e1c62ae4f..98f3e5dc9 100644 --- a/__tests__/children.test.ts +++ b/__tests__/children.test.ts @@ -80,11 +80,7 @@ describe('shapes with children', () => { type: MoveType.ToBack, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['3', '1', '2', '4']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '1', '2', '4']) }) it('moves two adjacent siblings to back', () => { @@ -92,11 +88,7 @@ describe('shapes with children', () => { type: MoveType.ToBack, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['2', '4', '3', '1']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '4', '3', '1']) }) it('moves two non-adjacent siblings to back', () => { @@ -104,11 +96,7 @@ describe('shapes with children', () => { type: MoveType.ToBack, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['4', '1', '2', '3']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '1', '2', '3']) }) it('moves a shape backward', () => { @@ -116,11 +104,7 @@ describe('shapes with children', () => { type: MoveType.Backward, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['4', '1', '3', '2']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '1', '3', '2']) }) it('moves a shape at first index backward', () => { @@ -128,11 +112,7 @@ describe('shapes with children', () => { type: MoveType.Backward, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['4', '1', '3', '2']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '1', '3', '2']) }) it('moves two adjacent siblings backward', () => { @@ -140,23 +120,15 @@ describe('shapes with children', () => { type: MoveType.Backward, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['4', '3', '2', '1']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '3', '2', '1']) }) it('moves two non-adjacent siblings backward', () => { - tt.clickShape('3').clickShape('1', { shiftKey: true }).send('MOVED', { - type: MoveType.Backward, - }) + tt.clickShape('3') + .clickShape('1', { shiftKey: true }) + .send('MOVED', { type: MoveType.Backward }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['3', '4', '1', '2']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '4', '1', '2']) }) it('moves two adjacent siblings backward at zero index', () => { @@ -164,11 +136,7 @@ describe('shapes with children', () => { type: MoveType.Backward, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['3', '4', '1', '2']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '4', '1', '2']) }) it('moves a shape forward', () => { @@ -176,11 +144,7 @@ describe('shapes with children', () => { type: MoveType.Forward, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['3', '1', '4', '2']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '1', '4', '2']) }) it('moves a shape forward at the top index', () => { @@ -188,11 +152,7 @@ describe('shapes with children', () => { type: MoveType.Forward, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['3', '1', '4', '2']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '1', '4', '2']) }) it('moves two adjacent siblings forward', () => { @@ -205,11 +165,7 @@ describe('shapes with children', () => { expect(tt.idsAreSelected(['1', '4'])).toBe(true) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['3', '2', '1', '4']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['3', '2', '1', '4']) }) it('moves two non-adjacent siblings forward', () => { @@ -220,11 +176,7 @@ describe('shapes with children', () => { type: MoveType.Forward, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['2', '3', '4', '1']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '3', '4', '1']) }) it('moves two adjacent siblings forward at top index', () => { @@ -234,11 +186,7 @@ describe('shapes with children', () => { .send('MOVED', { type: MoveType.Forward, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['2', '4', '3', '1']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '4', '3', '1']) }) it('moves a shape to front', () => { @@ -246,11 +194,7 @@ describe('shapes with children', () => { type: MoveType.ToFront, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['4', '3', '1', '2']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '3', '1', '2']) }) it('moves two adjacent siblings to front', () => { @@ -261,11 +205,7 @@ describe('shapes with children', () => { type: MoveType.ToFront, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['4', '2', '3', '1']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['4', '2', '3', '1']) }) it('moves two non-adjacent siblings to front', () => { @@ -276,11 +216,7 @@ describe('shapes with children', () => { type: MoveType.ToFront, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['2', '1', '4', '3']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '1', '4', '3']) }) it('moves siblings already at front to front', () => { @@ -291,10 +227,6 @@ describe('shapes with children', () => { type: MoveType.ToFront, }) - expect( - Object.values(tt.data.document.pages[tt.data.currentParentId].shapes) - .sort((a, b) => a.childIndex - b.childIndex) - .map((shape) => shape.id) - ).toStrictEqual(['2', '1', '4', '3']) + expect(tt.getSortedPageShapeIds()).toStrictEqual(['2', '1', '4', '3']) }) }) diff --git a/__tests__/dashes.test.ts b/__tests__/dashes.test.ts index e53264ba9..a6e923e00 100644 --- a/__tests__/dashes.test.ts +++ b/__tests__/dashes.test.ts @@ -1,32 +1,33 @@ +import { DashStyle } from 'types' import { getPerfectDashProps } from 'utils' describe('ellipse dash props', () => { it('renders dashed props on a circle correctly', () => { - expect(getPerfectDashProps(100, 4, 'dashed')).toMatchSnapshot( + expect(getPerfectDashProps(100, 4, DashStyle.Dashed)).toMatchSnapshot( 'small dashed circle dash props' ) - expect(getPerfectDashProps(100, 4, 'dashed')).toMatchSnapshot( + expect(getPerfectDashProps(100, 4, DashStyle.Dashed)).toMatchSnapshot( 'small dashed ellipse dash props' ) - expect(getPerfectDashProps(200, 8, 'dashed')).toMatchSnapshot( + expect(getPerfectDashProps(200, 8, DashStyle.Dashed)).toMatchSnapshot( 'large dashed circle dash props' ) - expect(getPerfectDashProps(200, 8, 'dashed')).toMatchSnapshot( + expect(getPerfectDashProps(200, 8, DashStyle.Dashed)).toMatchSnapshot( 'large dashed ellipse dash props' ) }) it('renders dotted props on a circle correctly', () => { - expect(getPerfectDashProps(100, 4, 'dotted')).toMatchSnapshot( + expect(getPerfectDashProps(100, 4, DashStyle.Dotted)).toMatchSnapshot( 'small dotted circle dash props' ) - expect(getPerfectDashProps(100, 4, 'dotted')).toMatchSnapshot( + expect(getPerfectDashProps(100, 4, DashStyle.Dotted)).toMatchSnapshot( 'small dotted ellipse dash props' ) - expect(getPerfectDashProps(200, 8, 'dotted')).toMatchSnapshot( + expect(getPerfectDashProps(200, 8, DashStyle.Dotted)).toMatchSnapshot( 'large dotted circle dash props' ) - expect(getPerfectDashProps(200, 8, 'dotted')).toMatchSnapshot( + expect(getPerfectDashProps(200, 8, DashStyle.Dotted)).toMatchSnapshot( 'large dotted ellipse dash props' ) }) diff --git a/__tests__/test-utils.ts b/__tests__/test-utils.ts index 6594d74df..f33827b6c 100644 --- a/__tests__/test-utils.ts +++ b/__tests__/test-utils.ts @@ -91,6 +91,23 @@ class TestState { return this } + /** + * Get the sorted ids of the page's children. + * + * ### Example + * + *```ts + * tt.getSortedPageShapes() + *``` + */ + getSortedPageShapeIds(): string[] { + return Object.values( + this.data.document.pages[this.data.currentParentId].shapes + ) + .sort((a, b) => a.childIndex - b.childIndex) + .map((shape) => shape.id) + } + /** * Get whether the provided ids are the current selected ids. If the `strict` argument is `true`, then the result will be false if the state has selected ids in addition to those provided. * diff --git a/components/code-panel/types-import.ts b/components/code-panel/types-import.ts index 4df0ea3dc..f5f09a967 100644 --- a/components/code-panel/types-import.ts +++ b/components/code-panel/types-import.ts @@ -49,6 +49,7 @@ enum SizeStyle { } enum DashStyle { + Draw = 'Draw', Solid = 'Solid', Dashed = 'Dashed', Dotted = 'Dotted', @@ -399,6 +400,9 @@ type PropsOfType> = { type Mutable = { -readonly [K in keyof T]: T[K] } interface ShapeUtility { + // Default properties when creating a new shape + defaultProps: K + // A cache for the computed bounds of this kind of shape. boundsCache: WeakMap @@ -424,7 +428,7 @@ interface ShapeUtility { isShy: boolean // Create a new shape. - create(props: Partial): K + create(this: ShapeUtility, props: Partial): K // Update a shape's styles applyStyles( @@ -612,6 +616,7 @@ enum SizeStyle { } enum DashStyle { + Draw = 'Draw', Solid = 'Solid', Dashed = 'Dashed', Dotted = 'Dotted', @@ -962,6 +967,9 @@ type PropsOfType> = { type Mutable = { -readonly [K in keyof T]: T[K] } interface ShapeUtility { + // Default properties when creating a new shape + defaultProps: K + // A cache for the computed bounds of this kind of shape. boundsCache: WeakMap @@ -987,7 +995,7 @@ interface ShapeUtility { isShy: boolean // Create a new shape. - create(props: Partial): K + create(this: ShapeUtility, props: Partial): K // Update a shape's styles applyStyles( diff --git a/components/shared.tsx b/components/shared.tsx index 42936727b..a4ee3215c 100644 --- a/components/shared.tsx +++ b/components/shared.tsx @@ -266,7 +266,12 @@ export const DropdownContent = styled(DropdownMenu.Content, { export function DashSolidIcon(): JSX.Element { return ( - + + + ) } + +export function DashDrawIcon(): JSX.Element { + return ( + + + + ) +} + +export function BoxIcon({ + fill = 'none', + stroke = 'black', +}: { + fill?: string + stroke?: string +}): JSX.Element { + return ( + + + + ) +} + +export function IsFilledFillIcon(): JSX.Element { + return ( + + + + ) +} + +export const ButtonsRow = styled('div', { + position: 'relative', + display: 'flex', + width: '100%', + background: 'none', + border: 'none', + cursor: 'pointer', + outline: 'none', + alignItems: 'center', + justifyContent: 'flex-start', + padding: 0, +}) diff --git a/components/style-panel/align-distribute.tsx b/components/style-panel/align-distribute.tsx index f6ab53d9c..c5af4acb5 100644 --- a/components/style-panel/align-distribute.tsx +++ b/components/style-panel/align-distribute.tsx @@ -10,10 +10,9 @@ import { StretchHorizontallyIcon, StretchVerticallyIcon, } from '@radix-ui/react-icons' -import { breakpoints, IconButton } from 'components/shared' +import { breakpoints, ButtonsRow, IconButton } from 'components/shared' import { memo } from 'react' import state from 'state' -import styled from 'styles' import { AlignType, DistributeType, StretchType } from 'types' function alignTop() { @@ -64,98 +63,93 @@ function AlignDistribute({ hasThreeOrMore: boolean }): JSX.Element { return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) } export default memo(AlignDistribute) - -const Container = styled('div', { - display: 'grid', - padding: 4, - gridTemplateColumns: 'repeat(5, auto)', - [`& ${IconButton} > svg`]: { - stroke: 'transparent', - }, -}) diff --git a/components/style-panel/dash-picker.tsx b/components/style-panel/dash-picker.tsx index 3de4d1ac9..9ab2d04d9 100644 --- a/components/style-panel/dash-picker.tsx +++ b/components/style-panel/dash-picker.tsx @@ -4,6 +4,7 @@ import { DashDashedIcon, DashDottedIcon, DashSolidIcon, + DashDrawIcon, } from '../shared' import * as RadioGroup from '@radix-ui/react-radio-group' import { DashStyle } from 'types' @@ -15,6 +16,7 @@ function handleChange(dash: string) { } const dashes = { + [DashStyle.Draw]: , [DashStyle.Solid]: , [DashStyle.Dashed]: , [DashStyle.Dotted]: , diff --git a/components/style-panel/quick-color-select.tsx b/components/style-panel/quick-color-select.tsx index e58ab898b..6ab7b79c6 100644 --- a/components/style-panel/quick-color-select.tsx +++ b/components/style-panel/quick-color-select.tsx @@ -2,9 +2,9 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu' import { breakpoints, IconButton } from 'components/shared' import Tooltip from 'components/tooltip' import { strokes } from 'state/shape-styles' -import { Square } from 'react-feather' import { useSelector } from 'state' import ColorContent from './color-content' +import { BoxIcon } from '../shared' export default function QuickColorSelect(): JSX.Element { const color = useSelector((s) => s.values.selectedStyle.color) @@ -13,7 +13,7 @@ export default function QuickColorSelect(): JSX.Element { - + diff --git a/components/style-panel/quick-dash-select.tsx b/components/style-panel/quick-dash-select.tsx index 5aca12488..c008a5828 100644 --- a/components/style-panel/quick-dash-select.tsx +++ b/components/style-panel/quick-dash-select.tsx @@ -7,12 +7,14 @@ import { DashStyle } from 'types' import { DropdownContent, Item, + DashDrawIcon, DashDottedIcon, DashSolidIcon, DashDashedIcon, } from '../shared' const dashes = { + [DashStyle.Draw]: , [DashStyle.Solid]: , [DashStyle.Dashed]: , [DashStyle.Dotted]: , diff --git a/components/style-panel/quick-fill-select.tsx b/components/style-panel/quick-fill-select.tsx new file mode 100644 index 000000000..19650750f --- /dev/null +++ b/components/style-panel/quick-fill-select.tsx @@ -0,0 +1,43 @@ +import * as Checkbox from '@radix-ui/react-checkbox' +import tld from 'utils/tld' +import { + breakpoints, + BoxIcon, + IsFilledFillIcon, + IconButton, + IconWrapper, +} from '../shared' +import state, { useSelector } from 'state' +import { getShapeUtils } from 'state/shape-utils' + +function handleIsFilledChange(isFilled: boolean) { + state.send('CHANGED_STYLE', { isFilled }) +} + +export default function IsFilledPicker(): JSX.Element { + const isFilled = useSelector((s) => s.values.selectedStyle.isFilled) + const canFill = useSelector((s) => { + const selectedShapes = tld.getSelectedShapes(s.data) + + return ( + selectedShapes.length === 0 || + selectedShapes.every((shape) => getShapeUtils(shape).canStyleFill) + ) + }) + + return ( + + + + + + + ) +} diff --git a/components/style-panel/shapes-functions.tsx b/components/style-panel/shapes-functions.tsx index ae28f049b..de21a7822 100644 --- a/components/style-panel/shapes-functions.tsx +++ b/components/style-panel/shapes-functions.tsx @@ -1,19 +1,16 @@ import tld from 'utils/tld' import state, { useSelector } from 'state' -import { IconButton, breakpoints } from 'components/shared' +import { IconButton, ButtonsRow, breakpoints } from 'components/shared' import { memo } from 'react' -import styled from 'styles' -import { MoveType } from 'types' +import { MoveType, ShapeType } from 'types' import { Trash2 } from 'react-feather' import Tooltip from 'components/tooltip' import { ArrowDownIcon, ArrowUpIcon, AspectRatioIcon, - BoxIcon, CopyIcon, - EyeClosedIcon, - EyeOpenIcon, + GroupIcon, LockClosedIcon, LockOpen1Icon, PinBottomIcon, @@ -29,8 +26,12 @@ function handleDuplicate() { state.send('DUPLICATED') } -function handleHide() { - state.send('TOGGLED_SHAPE_HIDE') +function handleGroup() { + state.send('GROUPED') +} + +function handleUngroup() { + state.send('UNGROUPED') } function handleLock() { @@ -74,15 +75,24 @@ function ShapesFunctions() { ) }) - const isAllHidden = useSelector((s) => { - const page = tld.getPage(s.data) - return s.values.selectedIds.every((id) => page.shapes[id].isHidden) + const isAllGrouped = useSelector((s) => { + const selectedShapes = tld.getSelectedShapes(s.data) + return selectedShapes.every( + (shape) => + shape.type === ShapeType.Group || + (shape.parentId === selectedShapes[0].parentId && + selectedShapes[0].parentId !== s.data.currentPageId) + ) }) const hasSelection = useSelector((s) => { return tld.getSelectedIds(s.data).size > 0 }) + const hasMultipleSelection = useSelector((s) => { + return tld.getSelectedIds(s.data).size > 1 + }) + return ( <> @@ -107,17 +117,6 @@ function ShapesFunctions() { - - - {isAllHidden ? : } - - - - {isAllLocked ? : } + {isAllLocked ? : } @@ -136,7 +135,18 @@ function ShapesFunctions() { onClick={handleAspectLock} > - {isAllAspectLocked ? : } + + + + + + + @@ -201,16 +211,3 @@ function ShapesFunctions() { } export default memo(ShapesFunctions) - -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/style-panel.tsx b/components/style-panel/style-panel.tsx index fd38a4026..5e229e1fc 100644 --- a/components/style-panel/style-panel.tsx +++ b/components/style-panel/style-panel.tsx @@ -2,18 +2,16 @@ 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 { IconButton, ButtonsRow } from 'components/shared' import { ChevronDown, X } from 'react-feather' import ShapesFunctions from './shapes-functions' import AlignDistribute from './align-distribute' -import SizePicker from './size-picker' -import DashPicker from './dash-picker' import QuickColorSelect from './quick-color-select' -import ColorPicker from './color-picker' -import IsFilledPicker from './is-filled-picker' import QuickSizeSelect from './quick-size-select' -import QuickdashSelect from './quick-dash-select' +import QuickDashSelect from './quick-dash-select' +import QuickFillSelect from './quick-fill-select' import Tooltip from 'components/tooltip' +import { motion } from 'framer-motion' const breakpoints = { '@initial': 'mobile', '@sm': 'small' } as any @@ -26,116 +24,70 @@ export default function StylePanel(): JSX.Element { return ( - {isOpen ? ( - - ) : ( - <> - - - - - - - - - - )} + + + + + + + {isOpen ? : } + + + {isOpen && } ) } -// This panel is going to be hard to keep cool, as we're selecting computed -// information, based on the user's current selection. We might have to keep -// track of this data manually within our state. - -function SelectedShapeStyles(): JSX.Element { +function SelectedShapeContent(): JSX.Element { const selectedShapesCount = useSelector((s) => s.values.selectedIds.length) return ( - - -

Style

- - - - - - - - - - - - - - - - - 1} - hasThreeOrMore={selectedShapesCount > 2} - /> - - + <> +
+ + 1} + hasThreeOrMore={selectedShapesCount > 2} + /> + ) } -const StylePanelRoot = styled(Panel.Root, { +const StylePanelRoot = styled(motion(Panel.Root), { minWidth: 1, - width: 184, - maxWidth: 184, + width: 'fit-content', + maxWidth: 'fit-content', overflow: 'hidden', position: 'relative', border: '1px solid $panel', boxShadow: '0px 2px 4px rgba(0,0,0,.2)', display: 'flex', + flexDirection: 'column', alignItems: 'center', pointerEvents: 'all', + padding: 2, + + '& hr': { + marginTop: 2, + marginBottom: 2, + marginLeft: '-2px', + border: 'none', + height: 1, + backgroundColor: '$brushFill', + width: 'calc(100% + 4px)', + }, variants: { isOpen: { true: {}, false: { - padding: 2, width: 'fit-content', }, }, }, }) - -const Content = styled(Panel.Content, { - padding: 8, -}) - -const Row = styled('div', { - position: 'relative', - display: 'flex', - width: '100%', - background: 'none', - border: 'none', - outline: 'none', - alignItems: 'center', - justifyContent: 'space-between', - padding: '4px 2px 4px 12px', - - '& label': { - fontFamily: '$ui', - fontWeight: 400, - fontSize: '$1', - margin: 0, - padding: 0, - }, - - '& > svg': { - position: 'relative', - }, -}) diff --git a/state/clipboard.ts b/state/clipboard.ts index 6a137f86c..c232ddc64 100644 --- a/state/clipboard.ts +++ b/state/clipboard.ts @@ -89,7 +89,10 @@ class Clipboard { // Take a snapshot of the element const s = new XMLSerializer() - const svgString = s.serializeToString(svg) + const svgString = s + .serializeToString(svg) + .replaceAll(' ', '') + .replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1') // Copy to clipboard! try { diff --git a/state/commands/group.ts b/state/commands/group.ts index 382be5e64..4ef835c3a 100644 --- a/state/commands/group.ts +++ b/state/commands/group.ts @@ -1,7 +1,7 @@ import Command from './command' import history from '../history' import { Data, GroupShape, ShapeType } from 'types' -import { deepClone, getCommonBounds } from 'utils' +import { deepClone, getCommonBounds, uniqueId } from 'utils' import tld from 'utils/tld' import { createShape, getShapeUtils } from 'state/shape-utils' import commands from '.' @@ -23,6 +23,7 @@ export default function groupCommand(data: Data): void { // Do we need to ungroup the selected shapes shapes, rather than group them? if (isAllSameParent && initialShapes[0]?.parentId !== currentPageId) { const parent = tld.getShape(data, initialShapes[0]?.parentId) as GroupShape + if (parent.children.length === initialShapes.length) { commands.ungroup(data) return @@ -62,6 +63,7 @@ export default function groupCommand(data: Data): void { } const newGroupShape = createShape(ShapeType.Group, { + id: uniqueId(), parentId: newGroupParentId, point: [commonBounds.minX, commonBounds.minY], size: [commonBounds.width, commonBounds.height], diff --git a/state/shape-styles.ts b/state/shape-styles.ts index a83746e64..9d9e21a78 100644 --- a/state/shape-styles.ts +++ b/state/shape-styles.ts @@ -37,12 +37,6 @@ const strokeWidths = { [SizeStyle.Large]: 8, } -const dashArrays = { - [DashStyle.Solid]: () => [1], - [DashStyle.Dashed]: (sw: number) => [sw * 2, sw * 4], - [DashStyle.Dotted]: (sw: number) => [0, sw * 3], -} - const fontSizes = { [SizeStyle.Small]: 24, [SizeStyle.Medium]: 48, @@ -54,13 +48,6 @@ export function getStrokeWidth(size: SizeStyle): number { return strokeWidths[size] } -export function getStrokeDashArray( - dash: DashStyle, - strokeWidth: number -): number[] { - return dashArrays[dash](strokeWidth) -} - export function getFontSize(size: SizeStyle): number { return fontSizes[size] } diff --git a/state/shape-utils/arrow.tsx b/state/shape-utils/arrow.tsx index 4bceb6571..99e1985bf 100644 --- a/state/shape-utils/arrow.tsx +++ b/state/shape-utils/arrow.tsx @@ -127,29 +127,24 @@ const arrow = registerShapeUtils({ if (isStraightLine) { const straight_sw = - strokeWidth * (style.dash === DashStyle.Solid && bend === 0 ? 1 : 1.618) + strokeWidth * + (style.dash === DashStyle.Draw && bend === 0 ? 0.9 : 1.618) - if (shape.style.dash === DashStyle.Solid && !pathCache.has(shape)) { + if (shape.style.dash === DashStyle.Draw && !pathCache.has(shape)) { renderFreehandArrowShaft(shape) } const path = - shape.style.dash === DashStyle.Solid + shape.style.dash === DashStyle.Draw ? pathCache.get(shape) : 'M' + start.point + 'L' + end.point - const { strokeDasharray, strokeDashoffset } = - shape.style.dash === DashStyle.Solid - ? { - strokeDasharray: 'none', - strokeDashoffset: '0', - } - : getPerfectDashProps( - arrowDist, - sw, - shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed', - 2 - ) + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + arrowDist, + sw, + shape.style.dash, + 2 + ) startAngle = Math.PI @@ -182,23 +177,17 @@ const arrow = registerShapeUtils({ const path = getArrowArcPath(start, end, circle, bend) - const { strokeDasharray, strokeDashoffset } = - shape.style.dash === DashStyle.Solid - ? { - strokeDasharray: 'none', - strokeDashoffset: '0', - } - : getPerfectDashProps( - getArcLength( - [circle[0], circle[1]], - circle[2], - start.point, - end.point - ) - 1, - sw, - shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed', - 2 - ) + const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( + getArcLength( + [circle[0], circle[1]], + circle[2], + start.point, + end.point + ) - 1, + sw, + shape.style.dash, + 2 + ) startAngle = vec.angle([circle[0], circle[1]], start.point) - @@ -520,26 +509,23 @@ function renderFreehandArrowShaft(shape: ArrowShape) { const strokeWidth = +getShapeStyle(style).strokeWidth * 2 - const m = vec.add( - vec.lrp(start.point, end.point, 0.25 + Math.abs(getRandom()) / 2), - [getRandom() * strokeWidth, getRandom() * strokeWidth] - ) + const st = Math.abs(getRandom()) const stroke = getStroke( [ - ...vec.pointsBetween(start.point, m), - ...vec.pointsBetween(m, end.point), + start.point, + ...vec.pointsBetween(start.point, end.point), end.point, end.point, end.point, ], { - size: strokeWidth * 0.82, - thinning: 0.6, - easing: (t) => t * t * t * t, - end: { taper: 4 + getRandom() * 4 }, - start: { taper: 4 + getRandom() * 4 }, - simulatePressure: false, + size: strokeWidth / 2, + thinning: 0.5 + getRandom() * 0.3, + easing: (t) => t * t, + end: { taper: 1 }, + start: { taper: 1 + 32 * (st * st * st) }, + simulatePressure: true, } ) @@ -570,10 +556,13 @@ function getArrowHeadPoints(shape: ArrowShape, point: number[], angle = 0) { const getRandom = rng(shape.id) return { - left: vec.add(point, vec.rot(v, Math.PI / 6 + (Math.PI / 8) * getRandom())), + left: vec.add( + point, + vec.rot(v, Math.PI / 6 + (Math.PI / 12) * getRandom()) + ), right: vec.add( point, - vec.rot(v, -(Math.PI / 6) + (Math.PI / 8) * getRandom()) + vec.rot(v, -(Math.PI / 6) + (Math.PI / 12) * getRandom()) ), } } diff --git a/state/shape-utils/ellipse.tsx b/state/shape-utils/ellipse.tsx index b4ddd3a5c..628e044fe 100644 --- a/state/shape-utils/ellipse.tsx +++ b/state/shape-utils/ellipse.tsx @@ -54,7 +54,7 @@ const ellipse = registerShapeUtils({ const rx = Math.max(0, radiusX - strokeWidth / 2) const ry = Math.max(0, radiusY - strokeWidth / 2) - if (style.dash === DashStyle.Solid) { + if (style.dash === DashStyle.Draw) { if (!pathCache.has(shape)) { renderPath(shape) } @@ -84,7 +84,7 @@ const ellipse = registerShapeUtils({ const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( perimeter, strokeWidth * 1.618, - shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed', + shape.style.dash, 4 ) diff --git a/state/shape-utils/rectangle.tsx b/state/shape-utils/rectangle.tsx index d45ac1617..7a67bf6a0 100644 --- a/state/shape-utils/rectangle.tsx +++ b/state/shape-utils/rectangle.tsx @@ -38,7 +38,7 @@ const rectangle = registerShapeUtils({ const styles = getShapeStyle(style) const strokeWidth = +styles.strokeWidth - if (style.dash === DashStyle.Solid) { + if (style.dash === DashStyle.Draw) { if (!pathCache.has(shape.size)) { renderPath(shape) } @@ -78,7 +78,7 @@ const rectangle = registerShapeUtils({ const { strokeDasharray, strokeDashoffset } = getPerfectDashProps( length, sw, - shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed' + shape.style.dash ) return ( diff --git a/state/state.ts b/state/state.ts index 122096bb8..dc79f64d5 100644 --- a/state/state.ts +++ b/state/state.ts @@ -751,7 +751,9 @@ const state = createState({ { if: 'isToolLocked', to: 'dot.creating', - else: { to: 'selecting' }, + else: { + to: 'selecting', + }, }, ], CANCELLED: { @@ -1152,20 +1154,6 @@ const state = createState({ }, actions: { // Networked Room - setRtStatus(data, payload: { id: string; status: string }) { - const { status } = payload - - if (!data.room) { - data.room = { - id: null, - status: '', - peers: {}, - } - } - - data.room.peers = {} - data.room.status = status - }, addRtShape(data, payload: { pageId: string; shape: Shape }) { const { pageId, shape } = payload // What if the page is in storage? @@ -1264,6 +1252,7 @@ const state = createState({ }) const siblings = tld.getChildren(data, shape.parentId) + const childIndex = siblings.length ? siblings[siblings.length - 1].childIndex + 1 : 1 diff --git a/types.ts b/types.ts index 42329b2e7..edcacc8f0 100644 --- a/types.ts +++ b/types.ts @@ -108,6 +108,7 @@ export enum SizeStyle { } export enum DashStyle { + Draw = 'Draw', Solid = 'Solid', Dashed = 'Dashed', Dotted = 'Dotted', diff --git a/utils/tld.ts b/utils/tld.ts index bb72ec7fe..50e6df399 100644 --- a/utils/tld.ts +++ b/utils/tld.ts @@ -317,6 +317,7 @@ export default class StateUtils { static getTopParentId(data: Data, id: string): string { const shape = this.getPage(data).shapes[id] + return shape.parentId === data.currentPageId || shape.parentId === data.currentParentId ? id diff --git a/utils/utils.ts b/utils/utils.ts index edc9cd671..9da20e06c 100644 --- a/utils/utils.ts +++ b/utils/utils.ts @@ -1,5 +1,5 @@ import React from 'react' -import { Bounds, Edge, Corner, BezierCurveSegment } from 'types' +import { Bounds, Edge, Corner, BezierCurveSegment, DashStyle } from 'types' import { v4 as uuid } from 'uuid' import vec from './vec' import _isMobile from 'ismobilejs' @@ -421,7 +421,7 @@ export function getArcLength( export function getPerfectDashProps( length: number, strokeWidth: number, - style: 'dashed' | 'dotted' = 'dashed', + style: DashStyle, snap = 1 ): { strokeDasharray: string @@ -431,7 +431,12 @@ export function getPerfectDashProps( let strokeDashoffset: string let ratio: number - if (style === 'dashed') { + if (style === DashStyle.Solid || style === DashStyle.Draw) { + return { + strokeDasharray: 'none', + strokeDashoffset: 'none', + } + } else if (style === DashStyle.Dashed) { dashLength = strokeWidth * 2 ratio = 1 strokeDashoffset = (dashLength / 2).toString() @@ -1726,7 +1731,7 @@ export function getSvgPathFromStroke(stroke: number[][]): string { ) d.push('Z') - return d.join(' ') + return d.join(' ').replaceAll(/(\s[0-9]*\.[0-9]{2})([0-9]*)\b/g, '$1') } export function debounce unknown>(