Updates style panel

This commit is contained in:
Steve Ruiz 2021-07-01 23:11:09 +01:00
parent 04efb6f880
commit 39b943248f
22 changed files with 421 additions and 408 deletions

View file

@ -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'])
})
})

View file

@ -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'
)
})

View file

@ -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.
*

View file

@ -49,6 +49,7 @@ enum SizeStyle {
}
enum DashStyle {
Draw = 'Draw',
Solid = 'Solid',
Dashed = 'Dashed',
Dotted = 'Dotted',
@ -399,6 +400,9 @@ type PropsOfType<T extends Record<string, unknown>> = {
type Mutable<T extends Shape> = { -readonly [K in keyof T]: T[K] }
interface ShapeUtility<K extends Shape> {
// Default properties when creating a new shape
defaultProps: K
// A cache for the computed bounds of this kind of shape.
boundsCache: WeakMap<K, Bounds>
@ -424,7 +428,7 @@ interface ShapeUtility<K extends Shape> {
isShy: boolean
// Create a new shape.
create(props: Partial<K>): K
create(this: ShapeUtility<K>, props: Partial<K>): 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<T extends Record<string, unknown>> = {
type Mutable<T extends Shape> = { -readonly [K in keyof T]: T[K] }
interface ShapeUtility<K extends Shape> {
// Default properties when creating a new shape
defaultProps: K
// A cache for the computed bounds of this kind of shape.
boundsCache: WeakMap<K, Bounds>
@ -987,7 +995,7 @@ interface ShapeUtility<K extends Shape> {
isShy: boolean
// Create a new shape.
create(props: Partial<K>): K
create(this: ShapeUtility<K>, props: Partial<K>): K
// Update a shape's styles
applyStyles(

File diff suppressed because one or more lines are too long

View file

@ -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 (
<Container>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignLeft}
>
<AlignLeftIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignCenterHorizontal}
>
<AlignCenterHorizontallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignRight}
>
<AlignRightIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={stretchHorizontally}
>
<StretchHorizontallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasThreeOrMore}
onClick={distributeHorizontally}
>
<SpaceEvenlyHorizontallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignTop}
>
<AlignTopIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignCenterVertical}
>
<AlignCenterVerticallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignBottom}
>
<AlignBottomIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={stretchVertically}
>
<StretchVerticallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasThreeOrMore}
onClick={distributeVertically}
>
<SpaceEvenlyVerticallyIcon />
</IconButton>
</Container>
<>
<ButtonsRow>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignLeft}
>
<AlignLeftIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignCenterHorizontal}
>
<AlignCenterHorizontallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignRight}
>
<AlignRightIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={stretchHorizontally}
>
<StretchHorizontallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasThreeOrMore}
onClick={distributeHorizontally}
>
<SpaceEvenlyHorizontallyIcon />
</IconButton>
</ButtonsRow>
<ButtonsRow>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignTop}
>
<AlignTopIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignCenterVertical}
>
<AlignCenterVerticallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={alignBottom}
>
<AlignBottomIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasTwoOrMore}
onClick={stretchVertically}
>
<StretchVerticallyIcon />
</IconButton>
<IconButton
bp={breakpoints}
size="small"
disabled={!hasThreeOrMore}
onClick={distributeVertically}
>
<SpaceEvenlyVerticallyIcon />
</IconButton>
</ButtonsRow>
</>
)
}
export default memo(AlignDistribute)
const Container = styled('div', {
display: 'grid',
padding: 4,
gridTemplateColumns: 'repeat(5, auto)',
[`& ${IconButton} > svg`]: {
stroke: 'transparent',
},
})

View file

@ -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]: <DashDrawIcon />,
[DashStyle.Solid]: <DashSolidIcon />,
[DashStyle.Dashed]: <DashDashedIcon />,
[DashStyle.Dotted]: <DashDottedIcon />,

View file

@ -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 {
<DropdownMenu.Root dir="ltr">
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
<Tooltip label="Color">
<Square fill={strokes[color]} stroke={strokes[color]} />
<BoxIcon fill={strokes[color]} stroke={strokes[color]} />
</Tooltip>
</DropdownMenu.Trigger>
<ColorContent />

View file

@ -7,12 +7,14 @@ import { DashStyle } from 'types'
import {
DropdownContent,
Item,
DashDrawIcon,
DashDottedIcon,
DashSolidIcon,
DashDashedIcon,
} from '../shared'
const dashes = {
[DashStyle.Draw]: <DashDrawIcon />,
[DashStyle.Solid]: <DashSolidIcon />,
[DashStyle.Dashed]: <DashDashedIcon />,
[DashStyle.Dotted]: <DashDottedIcon />,

View file

@ -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 (
<Checkbox.Root
dir="ltr"
as={IconButton}
bp={breakpoints}
checked={isFilled}
disabled={!canFill}
onCheckedChange={handleIsFilledChange}
>
<IconWrapper>
<BoxIcon />
<Checkbox.Indicator as={IsFilledFillIcon} />
</IconWrapper>
</Checkbox.Root>
)
}

View file

@ -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 (
<>
<ButtonsRow>
@ -107,17 +117,6 @@ function ShapesFunctions() {
</Tooltip>
</IconButton>
<IconButton
bp={breakpoints}
disabled={!hasSelection}
size="small"
onClick={handleHide}
>
<Tooltip label="Toogle Hidden">
{isAllHidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
</Tooltip>
</IconButton>
<IconButton
bp={breakpoints}
disabled={!hasSelection}
@ -125,7 +124,7 @@ function ShapesFunctions() {
onClick={handleLock}
>
<Tooltip label="Toogle Locked">
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon opacity={0.4} />}
</Tooltip>
</IconButton>
@ -136,7 +135,18 @@ function ShapesFunctions() {
onClick={handleAspectLock}
>
<Tooltip label="Toogle Aspect Ratio Lock">
{isAllAspectLocked ? <AspectRatioIcon /> : <BoxIcon />}
<AspectRatioIcon opacity={isAllAspectLocked ? 1 : 0.4} />
</Tooltip>
</IconButton>
<IconButton
bp={breakpoints}
disabled={!isAllGrouped && !hasMultipleSelection}
size="small"
onClick={isAllGrouped ? handleUngroup : handleGroup}
>
<Tooltip label="Group">
<GroupIcon opacity={isAllGrouped ? 1 : 0.4} />
</Tooltip>
</IconButton>
</ButtonsRow>
@ -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,
})

View file

@ -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 (
<StylePanelRoot dir="ltr" ref={rContainer} isOpen={isOpen}>
{isOpen ? (
<SelectedShapeStyles />
) : (
<>
<QuickColorSelect />
<QuickSizeSelect />
<QuickdashSelect />
<IconButton
bp={breakpoints}
title="Style"
size="small"
onClick={handleStylePanelOpen}
>
<Tooltip label="More">
<ChevronDown />
</Tooltip>
</IconButton>
</>
)}
<ButtonsRow>
<QuickColorSelect />
<QuickSizeSelect />
<QuickDashSelect />
<QuickFillSelect />
<IconButton
bp={breakpoints}
title="Style"
size="small"
onClick={handleStylePanelOpen}
>
<Tooltip label="More">{isOpen ? <X /> : <ChevronDown />}</Tooltip>
</IconButton>
</ButtonsRow>
{isOpen && <SelectedShapeContent />}
</StylePanelRoot>
)
}
// 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 (
<Panel.Layout>
<Panel.Header side="right">
<h3>Style</h3>
<IconButton
bp={breakpoints}
size="small"
onClick={handleStylePanelOpen}
>
<X />
</IconButton>
</Panel.Header>
<Content>
<ColorPicker />
<IsFilledPicker />
<Row>
<label htmlFor="size">Size</label>
<SizePicker />
</Row>
<Row>
<label htmlFor="dash">Dash</label>
<DashPicker />
</Row>
<ShapesFunctions />
<AlignDistribute
hasTwoOrMore={selectedShapesCount > 1}
hasThreeOrMore={selectedShapesCount > 2}
/>
</Content>
</Panel.Layout>
<>
<hr />
<ShapesFunctions />
<AlignDistribute
hasTwoOrMore={selectedShapesCount > 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',
},
})

View file

@ -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('&#10; ', '')
.replaceAll(/((\s|")[0-9]*\.[0-9]{2})([0-9]*)(\b|"|\))/g, '$1')
// Copy to clipboard!
try {

View file

@ -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],

View file

@ -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]
}

View file

@ -127,29 +127,24 @@ const arrow = registerShapeUtils<ArrowShape>({
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<ArrowShape>({
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())
),
}
}

View file

@ -54,7 +54,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
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<EllipseShape>({
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
perimeter,
strokeWidth * 1.618,
shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed',
shape.style.dash,
4
)

View file

@ -38,7 +38,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
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<RectangleShape>({
const { strokeDasharray, strokeDashoffset } = getPerfectDashProps(
length,
sw,
shape.style.dash === DashStyle.Dotted ? 'dotted' : 'dashed'
shape.style.dash
)
return (

View file

@ -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

View file

@ -108,6 +108,7 @@ export enum SizeStyle {
}
export enum DashStyle {
Draw = 'Draw',
Solid = 'Solid',
Dashed = 'Dashed',
Dotted = 'Dotted',

View file

@ -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

View file

@ -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<T extends (...args: unknown[]) => unknown>(