Adds dashes
This commit is contained in:
parent
ea996b627b
commit
815bf1109c
29 changed files with 698 additions and 233 deletions
|
@ -18,9 +18,9 @@ export default function Handles() {
|
||||||
selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
|
selectedIds.length === 1 && getPage(data).shapes[selectedIds[0]]
|
||||||
)
|
)
|
||||||
|
|
||||||
const isTranslatingHandles = useSelector((s) => s.isIn('translatingHandles'))
|
const isSelecting = useSelector((s) => s.isIn('selecting.notPointing'))
|
||||||
|
|
||||||
if (!shape.handles || isTranslatingHandles) return null
|
if (!shape.handles || !isSelecting) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
|
@ -57,7 +57,7 @@ function Handle({
|
||||||
pointerEvents="all"
|
pointerEvents="all"
|
||||||
transform={`translate(${point})`}
|
transform={`translate(${point})`}
|
||||||
>
|
>
|
||||||
<HandleCircleOuter r={8} />
|
<HandleCircleOuter r={12} />
|
||||||
<DotCircle r={4} />
|
<DotCircle r={4} />
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
|
|
|
@ -34,10 +34,20 @@ export default function Canvas() {
|
||||||
} else {
|
} else {
|
||||||
if (isMobile()) {
|
if (isMobile()) {
|
||||||
state.send('TOUCHED_CANVAS')
|
state.send('TOUCHED_CANVAS')
|
||||||
|
// state.send('POINTED_CANVAS', inputs.touchStart(e, 'canvas'))
|
||||||
|
// e.preventDefault()
|
||||||
|
// e.stopPropagation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
||||||
|
// if (!inputs.canAccept(e.touches[0].identifier)) return
|
||||||
|
// if (inputs.canAccept(e.touches[0].identifier)) {
|
||||||
|
// state.send('MOVED_POINTER', inputs.touchMove(e))
|
||||||
|
// }
|
||||||
|
// }, [])
|
||||||
|
|
||||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
if (inputs.canAccept(e.pointerId)) {
|
if (inputs.canAccept(e.pointerId)) {
|
||||||
|
@ -58,6 +68,7 @@ export default function Canvas() {
|
||||||
onPointerMove={handlePointerMove}
|
onPointerMove={handlePointerMove}
|
||||||
onPointerUp={handlePointerUp}
|
onPointerUp={handlePointerUp}
|
||||||
onTouchStart={handleTouchStart}
|
onTouchStart={handleTouchStart}
|
||||||
|
// onTouchMove={handleTouchMove}
|
||||||
>
|
>
|
||||||
<Defs />
|
<Defs />
|
||||||
{isReady && (
|
{isReady && (
|
||||||
|
|
|
@ -39,7 +39,7 @@ export function ShapeOutline({ id }: { id: string }) {
|
||||||
`
|
`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Indicator
|
<SelectIndicator
|
||||||
ref={rIndicator}
|
ref={rIndicator}
|
||||||
as="use"
|
as="use"
|
||||||
href={'#' + id}
|
href={'#' + id}
|
||||||
|
@ -50,13 +50,14 @@ export function ShapeOutline({ id }: { id: string }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Indicator = styled('path', {
|
const SelectIndicator = styled('path', {
|
||||||
zStrokeWidth: 1,
|
zStrokeWidth: 3,
|
||||||
strokeLineCap: 'round',
|
strokeLineCap: 'round',
|
||||||
strokeLinejoin: 'round',
|
strokeLinejoin: 'round',
|
||||||
stroke: '$selected',
|
stroke: '$selected',
|
||||||
fill: 'transparent',
|
fill: 'transparent',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'none',
|
||||||
|
paintOrder: 'stroke fill markers',
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
isLocked: {
|
isLocked: {
|
||||||
|
@ -65,5 +66,6 @@ const Indicator = styled('path', {
|
||||||
},
|
},
|
||||||
false: {},
|
false: {},
|
||||||
},
|
},
|
||||||
|
variant: {},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { useSelector } from 'state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { getPage } from 'utils/utils'
|
import { getPage } from 'utils/utils'
|
||||||
import { ShapeStyles } from 'types'
|
import { DashStyle, ShapeStyles } from 'types'
|
||||||
import useShapeEvents from 'hooks/useShapeEvents'
|
import useShapeEvents from 'hooks/useShapeEvents'
|
||||||
|
import { shades, strokes } from 'lib/colors'
|
||||||
|
|
||||||
function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
||||||
const isHovered = useSelector((state) => state.data.hoveredId === id)
|
const isHovered = useSelector((state) => state.data.hoveredId === id)
|
||||||
|
@ -35,36 +36,61 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
||||||
isHovered={isHovered}
|
isHovered={isHovered}
|
||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
transform={transform}
|
transform={transform}
|
||||||
{...events}
|
stroke={'red'}
|
||||||
|
strokeWidth={10}
|
||||||
>
|
>
|
||||||
{isSelecting && (
|
{isSelecting && (
|
||||||
<HoverIndicator
|
<HoverIndicator
|
||||||
as="use"
|
as="use"
|
||||||
href={'#' + id}
|
href={'#' + id}
|
||||||
strokeWidth={+shape.style.strokeWidth + 8}
|
strokeWidth={+shape.style.strokeWidth + 8}
|
||||||
|
variant={shape.style.fill === 'none' ? 'hollow' : 'filled'}
|
||||||
|
{...events}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!shape.isHidden && <StyledShape id={id} style={shape.style} />}
|
{!shape.isHidden && (
|
||||||
|
<RealShape id={id} style={sanitizeStyle(shape.style)} />
|
||||||
|
)}
|
||||||
</StyledGroup>
|
</StyledGroup>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledShape = memo(
|
const RealShape = memo(({ id, style }: { id: string; style: ShapeStyles }) => {
|
||||||
({ id, style }: { id: string; style: ShapeStyles }) => {
|
return (
|
||||||
return <use href={'#' + id} {...style} />
|
<StyledShape
|
||||||
}
|
as="use"
|
||||||
)
|
href={'#' + id}
|
||||||
|
{...style}
|
||||||
|
strokeDasharray={getDash(style.dash, +style.strokeWidth)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const StyledShape = styled('path', {
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
})
|
||||||
|
|
||||||
const HoverIndicator = styled('path', {
|
const HoverIndicator = styled('path', {
|
||||||
fill: 'none',
|
fill: 'transparent',
|
||||||
stroke: 'transparent',
|
stroke: 'transparent',
|
||||||
pointerEvents: 'all',
|
|
||||||
strokeLinecap: 'round',
|
strokeLinecap: 'round',
|
||||||
strokeLinejoin: 'round',
|
strokeLinejoin: 'round',
|
||||||
transform: 'all .2s',
|
transform: 'all .2s',
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
hollow: {
|
||||||
|
pointerEvents: 'stroke',
|
||||||
|
},
|
||||||
|
filled: {
|
||||||
|
pointerEvents: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const StyledGroup = styled('g', {
|
const StyledGroup = styled('g', {
|
||||||
|
pointerEvents: 'none',
|
||||||
[`& ${HoverIndicator}`]: {
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: '0',
|
opacity: '0',
|
||||||
},
|
},
|
||||||
|
@ -84,10 +110,8 @@ const StyledGroup = styled('g', {
|
||||||
isHovered: true,
|
isHovered: true,
|
||||||
css: {
|
css: {
|
||||||
[`& ${HoverIndicator}`]: {
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: '1',
|
opacity: '.4',
|
||||||
stroke: '$hint',
|
stroke: '$selected',
|
||||||
fill: '$hint',
|
|
||||||
// zStrokeWidth: [8, 4],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -96,10 +120,8 @@ const StyledGroup = styled('g', {
|
||||||
isHovered: false,
|
isHovered: false,
|
||||||
css: {
|
css: {
|
||||||
[`& ${HoverIndicator}`]: {
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: '1',
|
opacity: '.2',
|
||||||
stroke: '$hint',
|
stroke: '$selected',
|
||||||
fill: '$hint',
|
|
||||||
// zStrokeWidth: [6, 3],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -108,10 +130,8 @@ const StyledGroup = styled('g', {
|
||||||
isHovered: true,
|
isHovered: true,
|
||||||
css: {
|
css: {
|
||||||
[`& ${HoverIndicator}`]: {
|
[`& ${HoverIndicator}`]: {
|
||||||
opacity: '1',
|
opacity: '.2',
|
||||||
stroke: '$hint',
|
stroke: '$selected',
|
||||||
fill: '$hint',
|
|
||||||
// zStrokeWidth: [8, 4],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -134,6 +154,25 @@ function Label({ text }: { text: string }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDash(dash: DashStyle, s: number) {
|
||||||
|
switch (dash) {
|
||||||
|
case DashStyle.Solid: {
|
||||||
|
return 'none'
|
||||||
|
}
|
||||||
|
case DashStyle.Dashed: {
|
||||||
|
return `${s} ${s * 2}`
|
||||||
|
}
|
||||||
|
case DashStyle.Dotted: {
|
||||||
|
return `0 ${s * 1.5}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeStyle(style: ShapeStyles) {
|
||||||
|
const next = { ...style }
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
export { HoverIndicator }
|
export { HoverIndicator }
|
||||||
|
|
||||||
export default memo(Shape)
|
export default memo(Shape)
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default function Editor() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Layout = styled('div', {
|
const Layout = styled('main', {
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
|
@ -51,20 +51,24 @@ const Layout = styled('div', {
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const LeftPanels = styled('main', {
|
const LeftPanels = styled('div', {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridArea: 'leftPanels',
|
gridArea: 'leftPanels',
|
||||||
gridTemplateRows: '1fr auto',
|
gridTemplateRows: '1fr auto',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
zIndex: 250,
|
||||||
|
pointerEvents: 'none',
|
||||||
})
|
})
|
||||||
|
|
||||||
const RightPanels = styled('main', {
|
const RightPanels = styled('div', {
|
||||||
gridArea: 'rightPanels',
|
gridArea: 'rightPanels',
|
||||||
padding: 8,
|
padding: 8,
|
||||||
// display: 'grid',
|
display: 'grid',
|
||||||
// gridTemplateRows: 'auto',
|
gridTemplateRows: 'auto',
|
||||||
// height: 'fit-content',
|
height: 'fit-content',
|
||||||
// justifyContent: 'flex-end',
|
justifyContent: 'flex-end',
|
||||||
// gap: 8,
|
gap: 8,
|
||||||
|
zIndex: 300,
|
||||||
|
pointerEvents: 'none',
|
||||||
})
|
})
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default function ColorPicker({ colors, onChange, children }: Props) {
|
||||||
{children}
|
{children}
|
||||||
<Colors sideOffset={4}>
|
<Colors sideOffset={4}>
|
||||||
{Object.entries(colors).map(([name, color]) => (
|
{Object.entries(colors).map(([name, color]) => (
|
||||||
<ColorButton key={name} title={name} onSelect={() => onChange(color)}>
|
<ColorButton key={name} title={name} onSelect={() => onChange(name)}>
|
||||||
<ColorIcon color={color} />
|
<ColorIcon color={color} />
|
||||||
</ColorButton>
|
</ColorButton>
|
||||||
))}
|
))}
|
||||||
|
@ -29,7 +29,7 @@ export function ColorIcon({ color }: { color: string }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Colors = styled(DropdownMenu.Content, {
|
export const Colors = styled(DropdownMenu.Content, {
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
padding: 4,
|
padding: 4,
|
||||||
gridTemplateColumns: 'repeat(6, 1fr)',
|
gridTemplateColumns: 'repeat(6, 1fr)',
|
||||||
|
@ -117,4 +117,13 @@ export const CurrentColor = styled(DropdownMenu.Trigger, {
|
||||||
strokeWidth: 1,
|
strokeWidth: 1,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
size: {
|
||||||
|
icon: {
|
||||||
|
padding: '4px ',
|
||||||
|
width: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
64
components/style-panel/dash-picker.tsx
Normal file
64
components/style-panel/dash-picker.tsx
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
import { Group, RadioItem } from './shared'
|
||||||
|
import { DashStyle } from 'types'
|
||||||
|
import state from 'state'
|
||||||
|
import { ChangeEvent } from 'react'
|
||||||
|
|
||||||
|
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||||
|
state.send('CHANGED_STYLE', {
|
||||||
|
dash: e.currentTarget.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
dash: DashStyle
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DashPicker({ dash }: Props) {
|
||||||
|
return (
|
||||||
|
<Group name="Dash" onValueChange={handleChange}>
|
||||||
|
<RadioItem value={DashStyle.Solid} isActive={dash === DashStyle.Solid}>
|
||||||
|
<DashSolidIcon />
|
||||||
|
</RadioItem>
|
||||||
|
<RadioItem value={DashStyle.Dashed} isActive={dash === DashStyle.Dashed}>
|
||||||
|
<DashDashedIcon />
|
||||||
|
</RadioItem>
|
||||||
|
<RadioItem value={DashStyle.Dotted} isActive={dash === DashStyle.Dotted}>
|
||||||
|
<DashDottedIcon />
|
||||||
|
</RadioItem>
|
||||||
|
</Group>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DashSolidIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="16" height="16">
|
||||||
|
<path d="M 3,8 L 13,8" strokeWidth={3} strokeLinecap="round" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DashDashedIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="16" height="16">
|
||||||
|
<path
|
||||||
|
d="M 2,8 L 14,8"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="4 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function DashDottedIcon() {
|
||||||
|
return (
|
||||||
|
<svg width="16" height="16">
|
||||||
|
<path
|
||||||
|
d="M 3,8 L 14,8"
|
||||||
|
strokeWidth={3}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray="1 4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
76
components/style-panel/shared.tsx
Normal file
76
components/style-panel/shared.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||||
|
import * as Panel from '../panel'
|
||||||
|
import styled from 'styles'
|
||||||
|
|
||||||
|
export const StylePanelRoot = styled(Panel.Root, {
|
||||||
|
minWidth: 1,
|
||||||
|
width: 184,
|
||||||
|
maxWidth: 184,
|
||||||
|
overflow: 'hidden',
|
||||||
|
position: 'relative',
|
||||||
|
border: '1px solid $panel',
|
||||||
|
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isOpen: {
|
||||||
|
true: {},
|
||||||
|
false: {
|
||||||
|
padding: 2,
|
||||||
|
height: 38,
|
||||||
|
width: 38,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Group = styled(RadioGroup.Root, {
|
||||||
|
display: 'flex',
|
||||||
|
})
|
||||||
|
|
||||||
|
export 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': {
|
||||||
|
stroke: '$text',
|
||||||
|
fill: '$text',
|
||||||
|
strokeWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
'&:disabled': {
|
||||||
|
opacity: '0.5',
|
||||||
|
},
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isActive: {
|
||||||
|
true: {
|
||||||
|
'& svg': {
|
||||||
|
fill: '$text',
|
||||||
|
stroke: '$text',
|
||||||
|
strokeWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
'& svg': {
|
||||||
|
fill: '$inactive',
|
||||||
|
stroke: '$inactive',
|
||||||
|
strokeWidth: '0',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
|
@ -3,27 +3,22 @@ import state, { useSelector } from 'state'
|
||||||
import * as Panel from 'components/panel'
|
import * as Panel from 'components/panel'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { IconButton } from 'components/shared'
|
import { IconButton } from 'components/shared'
|
||||||
import { Circle, Copy, Lock, Trash, Trash2, Unlock, X } from 'react-feather'
|
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||||
import {
|
import { Trash2, X } from 'react-feather'
|
||||||
deepCompare,
|
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
|
||||||
deepCompareArrays,
|
|
||||||
getPage,
|
|
||||||
getSelectedShapes,
|
|
||||||
} from 'utils/utils'
|
|
||||||
import { shades, fills, strokes } from 'lib/colors'
|
import { shades, fills, strokes } from 'lib/colors'
|
||||||
|
|
||||||
import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
|
import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
|
||||||
import AlignDistribute from './align-distribute'
|
import AlignDistribute from './align-distribute'
|
||||||
import { MoveType, ShapeStyles } from 'types'
|
import { MoveType, ShapeStyles } from 'types'
|
||||||
import WidthPicker from './width-picker'
|
import WidthPicker from './width-picker'
|
||||||
import {
|
import {
|
||||||
AlignTopIcon,
|
|
||||||
ArrowDownIcon,
|
ArrowDownIcon,
|
||||||
ArrowUpIcon,
|
ArrowUpIcon,
|
||||||
AspectRatioIcon,
|
AspectRatioIcon,
|
||||||
BoxIcon,
|
BoxIcon,
|
||||||
|
CheckIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
DotsHorizontalIcon,
|
DotsVerticalIcon,
|
||||||
EyeClosedIcon,
|
EyeClosedIcon,
|
||||||
EyeOpenIcon,
|
EyeOpenIcon,
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
|
@ -31,11 +26,17 @@ import {
|
||||||
PinBottomIcon,
|
PinBottomIcon,
|
||||||
PinTopIcon,
|
PinTopIcon,
|
||||||
RotateCounterClockwiseIcon,
|
RotateCounterClockwiseIcon,
|
||||||
TrashIcon,
|
|
||||||
} from '@radix-ui/react-icons'
|
} from '@radix-ui/react-icons'
|
||||||
|
import DashPicker from './dash-picker'
|
||||||
|
|
||||||
const fillColors = { ...shades, ...fills }
|
const fillColors = { ...shades, ...fills }
|
||||||
const strokeColors = { ...shades, ...strokes }
|
const strokeColors = { ...shades, ...strokes }
|
||||||
|
const getFillColor = (color: string) => {
|
||||||
|
if (shades[color]) {
|
||||||
|
return '#fff'
|
||||||
|
}
|
||||||
|
return fillColors[color]
|
||||||
|
}
|
||||||
|
|
||||||
export default function StylePanel() {
|
export default function StylePanel() {
|
||||||
const rContainer = useRef<HTMLDivElement>(null)
|
const rContainer = useRef<HTMLDivElement>(null)
|
||||||
|
@ -46,14 +47,41 @@ export default function StylePanel() {
|
||||||
{isOpen ? (
|
{isOpen ? (
|
||||||
<SelectedShapeStyles />
|
<SelectedShapeStyles />
|
||||||
) : (
|
) : (
|
||||||
<IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
|
<>
|
||||||
<DotsHorizontalIcon />
|
<QuickColorSelect prop="stroke" colors={strokeColors} />
|
||||||
|
<IconButton
|
||||||
|
title="Style"
|
||||||
|
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
|
||||||
|
>
|
||||||
|
<DotsVerticalIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</StylePanelRoot>
|
</StylePanelRoot>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function QuickColorSelect({
|
||||||
|
prop,
|
||||||
|
colors,
|
||||||
|
}: {
|
||||||
|
prop: ShapeStyles['fill'] | ShapeStyles['stroke']
|
||||||
|
colors: Record<string, string>
|
||||||
|
}) {
|
||||||
|
const value = useSelector((s) => s.values.selectedStyle[prop])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ColorPicker
|
||||||
|
colors={colors}
|
||||||
|
onChange={(color) => state.send('CHANGED_STYLE', { [prop]: color })}
|
||||||
|
>
|
||||||
|
<CurrentColor size="icon" title={prop}>
|
||||||
|
<ColorIcon color={value} />
|
||||||
|
</CurrentColor>
|
||||||
|
</ColorPicker>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// This panel is going to be hard to keep cool, as we're selecting computed
|
// 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
|
// information, based on the user's current selection. We might have to keep
|
||||||
// track of this data manually within our state.
|
// track of this data manually within our state.
|
||||||
|
@ -79,33 +107,7 @@ function SelectedShapeStyles({}: {}) {
|
||||||
return selectedIds.every((id) => page.shapes[id].isHidden)
|
return selectedIds.every((id) => page.shapes[id].isHidden)
|
||||||
})
|
})
|
||||||
|
|
||||||
const commonStyle = useSelector((s) => {
|
const commonStyle = useSelector((s) => s.values.selectedStyle, deepCompare)
|
||||||
const { currentStyle } = s.data
|
|
||||||
|
|
||||||
if (selectedIds.length === 0) {
|
|
||||||
return currentStyle
|
|
||||||
}
|
|
||||||
const page = getPage(s.data)
|
|
||||||
const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
|
|
||||||
|
|
||||||
const commonStyle: Partial<ShapeStyles> = {}
|
|
||||||
const overrides = new Set<string>([])
|
|
||||||
|
|
||||||
for (const shapeStyle of shapeStyles) {
|
|
||||||
for (let key in currentStyle) {
|
|
||||||
if (overrides.has(key)) continue
|
|
||||||
if (commonStyle[key] === undefined) {
|
|
||||||
commonStyle[key] = shapeStyle[key]
|
|
||||||
} else {
|
|
||||||
if (commonStyle[key] === shapeStyle[key]) continue
|
|
||||||
commonStyle[key] = currentStyle[key]
|
|
||||||
overrides.add(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return commonStyle
|
|
||||||
}, deepCompare)
|
|
||||||
|
|
||||||
const hasSelection = selectedIds.length > 0
|
const hasSelection = selectedIds.length > 0
|
||||||
|
|
||||||
|
@ -118,28 +120,42 @@ function SelectedShapeStyles({}: {}) {
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Panel.Header>
|
</Panel.Header>
|
||||||
<Content>
|
<Content>
|
||||||
<ColorPicker
|
|
||||||
colors={fillColors}
|
|
||||||
onChange={(color) => state.send('CHANGED_STYLE', { fill: color })}
|
|
||||||
>
|
|
||||||
<CurrentColor>
|
|
||||||
<label>Fill</label>
|
|
||||||
<ColorIcon color={commonStyle.fill} />
|
|
||||||
</CurrentColor>
|
|
||||||
</ColorPicker>
|
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
colors={strokeColors}
|
colors={strokeColors}
|
||||||
onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
|
onChange={(color) =>
|
||||||
|
state.send('CHANGED_STYLE', {
|
||||||
|
stroke: strokeColors[color],
|
||||||
|
fill: getFillColor(color),
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<CurrentColor>
|
<CurrentColor>
|
||||||
<label>Stroke</label>
|
<label>Color</label>
|
||||||
<ColorIcon color={commonStyle.stroke} />
|
<ColorIcon color={commonStyle.stroke} />
|
||||||
</CurrentColor>
|
</CurrentColor>
|
||||||
</ColorPicker>
|
</ColorPicker>
|
||||||
|
{/* <Row>
|
||||||
|
<label htmlFor="filled">Filled</label>
|
||||||
|
<StyledCheckbox
|
||||||
|
checked={commonStyle.isFilled}
|
||||||
|
onCheckedChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
console.log(e.target.value)
|
||||||
|
state.send('CHANGED_STYLE', {
|
||||||
|
isFilled: e.target.value === 'on',
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox.Indicator as={CheckIcon} />
|
||||||
|
</StyledCheckbox>
|
||||||
|
</Row>*/}
|
||||||
<Row>
|
<Row>
|
||||||
<label htmlFor="width">Width</label>
|
<label htmlFor="width">Width</label>
|
||||||
<WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
|
<WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
|
||||||
</Row>
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<label htmlFor="dash">Dash</label>
|
||||||
|
<DashPicker dash={commonStyle.dash} />
|
||||||
|
</Row>
|
||||||
<ButtonsRow>
|
<ButtonsRow>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={!hasSelection}
|
disabled={!hasSelection}
|
||||||
|
@ -221,14 +237,16 @@ const StylePanelRoot = styled(Panel.Root, {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
border: '1px solid $panel',
|
border: '1px solid $panel',
|
||||||
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
|
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
isOpen: {
|
isOpen: {
|
||||||
true: {},
|
true: {},
|
||||||
false: {
|
false: {
|
||||||
padding: 2,
|
padding: 2,
|
||||||
height: 38,
|
width: 'fit-content',
|
||||||
width: 38,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -275,3 +293,22 @@ const ButtonsRow = styled('div', {
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
padding: 4,
|
padding: 4,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const StyledCheckbox = styled(Checkbox.Root, {
|
||||||
|
appearance: 'none',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
border: 'none',
|
||||||
|
padding: 0,
|
||||||
|
boxShadow: 'inset 0 0 0 1px gainsboro',
|
||||||
|
width: 15,
|
||||||
|
height: 15,
|
||||||
|
borderRadius: 2,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
|
||||||
|
'&:focus': {
|
||||||
|
outline: 'none',
|
||||||
|
boxShadow: 'inset 0 0 0 1px dodgerblue, 0 0 0 1px dodgerblue',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
import { Group, RadioItem } from './shared'
|
||||||
import { ChangeEvent } from 'react'
|
import { ChangeEvent } from 'react'
|
||||||
import { Circle } from 'react-feather'
|
import { Circle } from 'react-feather'
|
||||||
import state from 'state'
|
import state from 'state'
|
||||||
import styled from 'styles'
|
|
||||||
|
|
||||||
function setWidth(e: ChangeEvent<HTMLInputElement>) {
|
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||||
state.send('CHANGED_STYLE', {
|
state.send('CHANGED_STYLE', {
|
||||||
strokeWidth: Number(e.currentTarget.value),
|
strokeWidth: Number(e.currentTarget.value),
|
||||||
})
|
})
|
||||||
|
@ -16,7 +15,7 @@ export default function WidthPicker({
|
||||||
strokeWidth?: number
|
strokeWidth?: number
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Group name="width" onValueChange={setWidth}>
|
<Group name="width" onValueChange={handleChange}>
|
||||||
<RadioItem value="2" isActive={strokeWidth === 2}>
|
<RadioItem value="2" isActive={strokeWidth === 2}>
|
||||||
<Circle size={6} />
|
<Circle size={6} />
|
||||||
</RadioItem>
|
</RadioItem>
|
||||||
|
@ -29,52 +28,3 @@ export default function WidthPicker({
|
||||||
</Group>
|
</Group>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
|
@ -23,16 +23,16 @@ export const strokes = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fills = {
|
export const fills = {
|
||||||
lime: 'rgba(217, 245, 162, 1.000)',
|
lime: 'rgba(243, 252, 227, 1.000)',
|
||||||
green: 'rgba(177, 242, 188, 1.000)',
|
green: 'rgba(235, 251, 238, 1.000)',
|
||||||
teal: 'rgba(149, 242, 215, 1.000)',
|
teal: 'rgba(230, 252, 245, 1.000)',
|
||||||
cyan: 'rgba(153, 233, 242, 1.000)',
|
cyan: 'rgba(227, 250, 251, 1.000)',
|
||||||
blue: 'rgba(166, 216, 255, 1.000)',
|
blue: 'rgba(231, 245, 255, 1.000)',
|
||||||
indigo: 'rgba(186, 200, 255, 1.000)',
|
indigo: 'rgba(237, 242, 255, 1.000)',
|
||||||
violet: 'rgba(208, 191, 255, 1.000)',
|
violet: 'rgba(242, 240, 255, 1.000)',
|
||||||
grape: 'rgba(237, 190, 250, 1.000)',
|
grape: 'rgba(249, 240, 252, 1.000)',
|
||||||
pink: 'rgba(252, 194, 215, 1.000)',
|
pink: 'rgba(254, 241, 246, 1.000)',
|
||||||
red: 'rgba(255, 201, 201, 1.000)',
|
red: 'rgba(255, 245, 245, 1.000)',
|
||||||
orange: 'rgba(255, 216, 168, 1.000)',
|
orange: 'rgba(255, 244, 229, 1.000)',
|
||||||
yellow: 'rgba(255, 236, 153, 1.000)',
|
yellow: 'rgba(255, 249, 219, 1.000)',
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,14 +3,29 @@ import * as vec from 'utils/vec'
|
||||||
import * as svg from 'utils/svg'
|
import * as svg from 'utils/svg'
|
||||||
import { ArrowShape, ShapeHandle, ShapeType } from 'types'
|
import { ArrowShape, ShapeHandle, ShapeType } from 'types'
|
||||||
import { registerShapeUtils } from './index'
|
import { registerShapeUtils } from './index'
|
||||||
import { circleFromThreePoints, clamp, getSweep } from 'utils/utils'
|
import { circleFromThreePoints, clamp, isAngleBetween } from 'utils/utils'
|
||||||
import { boundsContained } from 'utils/bounds'
|
import { pointInBounds } from 'utils/bounds'
|
||||||
import { intersectCircleBounds } from 'utils/intersections'
|
import {
|
||||||
|
intersectArcBounds,
|
||||||
|
intersectLineSegmentBounds,
|
||||||
|
} from 'utils/intersections'
|
||||||
import { getBoundsFromPoints, translateBounds } from 'utils/utils'
|
import { getBoundsFromPoints, translateBounds } from 'utils/utils'
|
||||||
import { pointInCircle } from 'utils/hitTests'
|
import { pointInCircle } from 'utils/hitTests'
|
||||||
|
|
||||||
const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
|
const ctpCache = new WeakMap<ArrowShape['handles'], number[]>()
|
||||||
|
|
||||||
|
function getCtp(shape: ArrowShape) {
|
||||||
|
if (!ctpCache.has(shape.handles)) {
|
||||||
|
const { start, end, bend } = shape.handles
|
||||||
|
ctpCache.set(
|
||||||
|
shape.handles,
|
||||||
|
circleFromThreePoints(start.point, end.point, bend.point)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctpCache.get(shape.handles)
|
||||||
|
}
|
||||||
|
|
||||||
const arrow = registerShapeUtils<ArrowShape>({
|
const arrow = registerShapeUtils<ArrowShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
|
@ -69,7 +84,8 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render({ id, bend, points, handles, style }) {
|
render(shape) {
|
||||||
|
const { id, bend, points, handles, style } = shape
|
||||||
const { start, end, bend: _bend } = handles
|
const { start, end, bend: _bend } = handles
|
||||||
|
|
||||||
const arrowDist = vec.dist(start.point, end.point)
|
const arrowDist = vec.dist(start.point, end.point)
|
||||||
|
@ -91,7 +107,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const circle = showCircle && ctpCache.get(handles)
|
const circle = showCircle && getCtp(shape)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<g id={id}>
|
<g id={id}>
|
||||||
|
@ -114,12 +130,14 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
cy={start.point[1]}
|
cy={start.point[1]}
|
||||||
r={+style.strokeWidth}
|
r={+style.strokeWidth}
|
||||||
fill={style.stroke}
|
fill={style.stroke}
|
||||||
|
strokeDasharray="none"
|
||||||
/>
|
/>
|
||||||
<polyline
|
<polyline
|
||||||
points={[b, points[1], c].join()}
|
points={[b, points[1], c].join()}
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
fill="none"
|
fill="none"
|
||||||
|
strokeDasharray="none"
|
||||||
/>
|
/>
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
|
@ -127,6 +145,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
|
|
||||||
applyStyles(shape, style) {
|
applyStyles(shape, style) {
|
||||||
Object.assign(shape.style, style)
|
Object.assign(shape.style, style)
|
||||||
|
shape.style.fill = 'none'
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -159,24 +178,29 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctpCache.has(shape.handles)) {
|
const [cx, cy, r] = getCtp(shape)
|
||||||
ctpCache.set(
|
|
||||||
shape.handles,
|
|
||||||
circleFromThreePoints(start.point, end.point, bend.point)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [cx, cy, r] = ctpCache.get(shape.handles)
|
|
||||||
|
|
||||||
return !pointInCircle(point, vec.add(shape.point, [cx, cy]), r - 4)
|
return !pointInCircle(point, vec.add(shape.point, [cx, cy]), r - 4)
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTestBounds(this, shape, brushBounds) {
|
hitTestBounds(this, shape, brushBounds) {
|
||||||
const shapeBounds = this.getBounds(shape)
|
const { start, end, bend } = shape.handles
|
||||||
return (
|
|
||||||
boundsContained(shapeBounds, brushBounds) ||
|
const sp = vec.add(shape.point, start.point)
|
||||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0
|
const ep = vec.add(shape.point, end.point)
|
||||||
)
|
|
||||||
|
if (pointInBounds(sp, brushBounds) || pointInBounds(ep, brushBounds)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vec.isEqual(vec.med(start.point, end.point), bend.point)) {
|
||||||
|
return intersectLineSegmentBounds(sp, ep, brushBounds).length > 0
|
||||||
|
} else {
|
||||||
|
const [cx, cy, r] = getCtp(shape)
|
||||||
|
const cp = vec.add(shape.point, [cx, cy])
|
||||||
|
|
||||||
|
return intersectArcBounds(sp, ep, cp, r, brushBounds).length > 0
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
rotateTo(shape, rotation) {
|
rotateTo(shape, rotation) {
|
||||||
|
@ -219,14 +243,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
start.point = shape.points[0]
|
start.point = shape.points[0]
|
||||||
end.point = shape.points[1]
|
end.point = shape.points[1]
|
||||||
|
|
||||||
const bendDist = (vec.dist(start.point, end.point) / 2) * shape.bend
|
bend.point = getBendPoint(shape)
|
||||||
const midPoint = vec.med(start.point, end.point)
|
|
||||||
const u = vec.uni(vec.vec(start.point, end.point))
|
|
||||||
|
|
||||||
bend.point =
|
|
||||||
Math.abs(bendDist) > 10
|
|
||||||
? vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
|
||||||
: midPoint
|
|
||||||
|
|
||||||
shape.points = [shape.handles.start.point, shape.handles.end.point]
|
shape.points = [shape.handles.start.point, shape.handles.end.point]
|
||||||
|
|
||||||
|
@ -244,8 +261,6 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
},
|
},
|
||||||
|
|
||||||
onHandleMove(shape, handles) {
|
onHandleMove(shape, handles) {
|
||||||
const { start, end, bend } = shape.handles
|
|
||||||
|
|
||||||
for (let id in handles) {
|
for (let id in handles) {
|
||||||
const handle = handles[id]
|
const handle = handles[id]
|
||||||
|
|
||||||
|
@ -255,38 +270,35 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
shape.points[handle.index] = handle.point
|
shape.points[handle.index] = handle.point
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { start, end, bend } = shape.handles
|
||||||
|
|
||||||
const dist = vec.dist(start.point, end.point)
|
const dist = vec.dist(start.point, end.point)
|
||||||
|
|
||||||
if (handle.id === 'bend') {
|
if (handle.id === 'bend') {
|
||||||
const distance = vec.distanceToLineSegment(
|
|
||||||
start.point,
|
|
||||||
end.point,
|
|
||||||
handle.point,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
shape.bend = clamp(distance / (dist / 2), -1, 1)
|
|
||||||
|
|
||||||
const a0 = vec.angle(handle.point, end.point)
|
|
||||||
const a1 = vec.angle(start.point, end.point)
|
|
||||||
if (a0 - a1 < 0) shape.bend *= -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const dist = vec.dist(start.point, end.point)
|
|
||||||
const midPoint = vec.med(start.point, end.point)
|
const midPoint = vec.med(start.point, end.point)
|
||||||
const bendDist = (dist / 2) * shape.bend
|
|
||||||
const u = vec.uni(vec.vec(start.point, end.point))
|
const u = vec.uni(vec.vec(start.point, end.point))
|
||||||
|
const ap = vec.add(midPoint, vec.mul(vec.per(u), dist / 2))
|
||||||
|
const bp = vec.sub(midPoint, vec.mul(vec.per(u), dist / 2))
|
||||||
|
|
||||||
shape.handles.bend.point =
|
bend.point = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
||||||
Math.abs(bendDist) > 10
|
shape.bend = vec.dist(bend.point, midPoint) / (dist / 2)
|
||||||
? vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
|
||||||
: midPoint
|
const sa = vec.angle(end.point, start.point)
|
||||||
|
const la = sa - Math.PI / 2
|
||||||
|
if (isAngleBetween(sa, la, vec.angle(end.point, bend.point))) {
|
||||||
|
shape.bend *= -1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shape.handles.bend.point = getBendPoint(shape)
|
||||||
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
canChangeAspectRatio: true,
|
canChangeAspectRatio: true,
|
||||||
|
canStyleFill: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default arrow
|
export default arrow
|
||||||
|
@ -311,3 +323,16 @@ function getArrowArcPath(
|
||||||
end.point[1],
|
end.point[1],
|
||||||
].join(' ')
|
].join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBendPoint(shape: ArrowShape) {
|
||||||
|
const { start, end, bend } = shape.handles
|
||||||
|
|
||||||
|
const dist = vec.dist(start.point, end.point)
|
||||||
|
const midPoint = vec.med(start.point, end.point)
|
||||||
|
const bendDist = (dist / 2) * shape.bend
|
||||||
|
const u = vec.uni(vec.vec(start.point, end.point))
|
||||||
|
|
||||||
|
return Math.abs(bendDist) < 10
|
||||||
|
? midPoint
|
||||||
|
: vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
||||||
|
}
|
||||||
|
|
|
@ -135,6 +135,7 @@ const circle = registerShapeUtils<CircleShape>({
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
canChangeAspectRatio: false,
|
canChangeAspectRatio: false,
|
||||||
|
canStyleFill: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default circle
|
export default circle
|
||||||
|
|
|
@ -104,6 +104,7 @@ const dot = registerShapeUtils<DotShape>({
|
||||||
|
|
||||||
canTransform: false,
|
canTransform: false,
|
||||||
canChangeAspectRatio: false,
|
canChangeAspectRatio: false,
|
||||||
|
canStyleFill: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default dot
|
export default dot
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
getSvgPathFromStroke,
|
getSvgPathFromStroke,
|
||||||
translateBounds,
|
translateBounds,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
|
import styled from 'styles'
|
||||||
|
|
||||||
const pathCache = new WeakMap<DrawShape['points'], string>([])
|
const pathCache = new WeakMap<DrawShape['points'], string>([])
|
||||||
|
|
||||||
|
@ -190,6 +191,11 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
canChangeAspectRatio: true,
|
canChangeAspectRatio: true,
|
||||||
|
canStyleFill: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default draw
|
export default draw
|
||||||
|
|
||||||
|
const DrawPath = styled('path', {
|
||||||
|
strokeWidth: 0,
|
||||||
|
})
|
||||||
|
|
|
@ -149,6 +149,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
canChangeAspectRatio: true,
|
canChangeAspectRatio: true,
|
||||||
|
canStyleFill: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default ellipse
|
export default ellipse
|
||||||
|
|
|
@ -39,6 +39,9 @@ export interface ShapeUtility<K extends Readonly<Shape>> {
|
||||||
// Whether the shape's aspect ratio can change
|
// Whether the shape's aspect ratio can change
|
||||||
canChangeAspectRatio: boolean
|
canChangeAspectRatio: boolean
|
||||||
|
|
||||||
|
// Whether the shape's style can be filled
|
||||||
|
canStyleFill: boolean
|
||||||
|
|
||||||
// Create a new shape.
|
// Create a new shape.
|
||||||
create(props: Partial<K>): K
|
create(props: Partial<K>): K
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ const line = registerShapeUtils<LineShape>({
|
||||||
|
|
||||||
canTransform: false,
|
canTransform: false,
|
||||||
canChangeAspectRatio: false,
|
canChangeAspectRatio: false,
|
||||||
|
canStyleFill: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default line
|
export default line
|
||||||
|
|
|
@ -137,6 +137,7 @@ const polyline = registerShapeUtils<PolylineShape>({
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
canChangeAspectRatio: true,
|
canChangeAspectRatio: true,
|
||||||
|
canStyleFill: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default polyline
|
export default polyline
|
||||||
|
|
|
@ -112,6 +112,7 @@ const ray = registerShapeUtils<RayShape>({
|
||||||
|
|
||||||
canTransform: false,
|
canTransform: false,
|
||||||
canChangeAspectRatio: false,
|
canChangeAspectRatio: false,
|
||||||
|
canStyleFill: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default ray
|
export default ray
|
||||||
|
|
|
@ -150,6 +150,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
|
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
canChangeAspectRatio: true,
|
canChangeAspectRatio: true,
|
||||||
|
canStyleFill: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
export default rectangle
|
export default rectangle
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@monaco-editor/react": "^4.1.3",
|
"@monaco-editor/react": "^4.1.3",
|
||||||
|
"@radix-ui/react-checkbox": "^0.0.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^0.0.19",
|
"@radix-ui/react-dropdown-menu": "^0.0.19",
|
||||||
"@radix-ui/react-icons": "^1.0.3",
|
"@radix-ui/react-icons": "^1.0.3",
|
||||||
"@radix-ui/react-radio-group": "^0.0.16",
|
"@radix-ui/react-radio-group": "^0.0.16",
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React from 'react'
|
||||||
import { PointerInfo } from 'types'
|
import { PointerInfo } from 'types'
|
||||||
import { isDarwin } from 'utils/utils'
|
import { isDarwin } from 'utils/utils'
|
||||||
|
|
||||||
|
@ -5,6 +6,52 @@ class Inputs {
|
||||||
activePointerId?: number
|
activePointerId?: number
|
||||||
points: Record<string, PointerInfo> = {}
|
points: Record<string, PointerInfo> = {}
|
||||||
|
|
||||||
|
touchStart(e: TouchEvent | React.TouchEvent, target: string) {
|
||||||
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
|
|
||||||
|
const touch = e.changedTouches[0]
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
target,
|
||||||
|
pointerId: touch.identifier,
|
||||||
|
origin: [touch.clientX, touch.clientY],
|
||||||
|
point: [touch.clientX, touch.clientY],
|
||||||
|
shiftKey,
|
||||||
|
ctrlKey,
|
||||||
|
metaKey: isDarwin() ? metaKey : ctrlKey,
|
||||||
|
altKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.points[touch.identifier] = info
|
||||||
|
this.activePointerId = touch.identifier
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
touchMove(e: TouchEvent | React.TouchEvent) {
|
||||||
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
|
|
||||||
|
const touch = e.changedTouches[0]
|
||||||
|
|
||||||
|
const prev = this.points[touch.identifier]
|
||||||
|
|
||||||
|
const info = {
|
||||||
|
...prev,
|
||||||
|
pointerId: touch.identifier,
|
||||||
|
point: [touch.clientX, touch.clientY],
|
||||||
|
shiftKey,
|
||||||
|
ctrlKey,
|
||||||
|
metaKey: isDarwin() ? metaKey : ctrlKey,
|
||||||
|
altKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.points[touch.identifier]) {
|
||||||
|
this.points[touch.identifier] = info
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
pointerDown(e: PointerEvent | React.PointerEvent, target: string) {
|
pointerDown(e: PointerEvent | React.PointerEvent, target: string) {
|
||||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
getCurrent,
|
getCurrent,
|
||||||
getPage,
|
getPage,
|
||||||
getSelectedBounds,
|
getSelectedBounds,
|
||||||
|
getSelectedShapes,
|
||||||
getShape,
|
getShape,
|
||||||
screenToWorld,
|
screenToWorld,
|
||||||
setZoomCSS,
|
setZoomCSS,
|
||||||
|
@ -32,6 +33,7 @@ import {
|
||||||
DistributeType,
|
DistributeType,
|
||||||
AlignType,
|
AlignType,
|
||||||
StretchType,
|
StretchType,
|
||||||
|
DashStyle,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
|
@ -50,6 +52,7 @@ const initialData: Data = {
|
||||||
fill: shades.lightGray,
|
fill: shades.lightGray,
|
||||||
stroke: shades.darkGray,
|
stroke: shades.darkGray,
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
|
dash: DashStyle.Solid,
|
||||||
},
|
},
|
||||||
camera: {
|
camera: {
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
|
@ -1296,6 +1299,35 @@ const state = createState({
|
||||||
...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
|
...shapes.map((shape) => getShapeUtils(shape).getRotatedBounds(shape))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
selectedStyle(data) {
|
||||||
|
const selectedIds = Array.from(data.selectedIds.values())
|
||||||
|
const { currentStyle } = data
|
||||||
|
|
||||||
|
if (selectedIds.length === 0) {
|
||||||
|
return currentStyle
|
||||||
|
}
|
||||||
|
const page = getPage(data)
|
||||||
|
const shapeStyles = selectedIds.map((id) => page.shapes[id].style)
|
||||||
|
|
||||||
|
const commonStyle: Partial<ShapeStyles> = {}
|
||||||
|
|
||||||
|
const overrides = new Set<string>([])
|
||||||
|
|
||||||
|
for (const shapeStyle of shapeStyles) {
|
||||||
|
for (let key in currentStyle) {
|
||||||
|
if (overrides.has(key)) continue
|
||||||
|
if (commonStyle[key] === undefined) {
|
||||||
|
commonStyle[key] = shapeStyle[key]
|
||||||
|
} else {
|
||||||
|
if (commonStyle[key] === shapeStyle[key]) continue
|
||||||
|
commonStyle[key] = currentStyle[key]
|
||||||
|
overrides.add(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commonStyle
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ const { styled, global, css, theme, getCssString } = createCss({
|
||||||
colors: {
|
colors: {
|
||||||
brushFill: 'rgba(0,0,0,.1)',
|
brushFill: 'rgba(0,0,0,.1)',
|
||||||
brushStroke: 'rgba(0,0,0,.5)',
|
brushStroke: 'rgba(0,0,0,.5)',
|
||||||
hint: 'rgba(66, 133, 244, 0.200)',
|
hint: 'rgba(216, 226, 249, 1.000)',
|
||||||
selected: 'rgba(66, 133, 244, 1.000)',
|
selected: 'rgba(66, 133, 244, 1.000)',
|
||||||
bounds: 'rgba(65, 132, 244, 1.000)',
|
bounds: 'rgba(65, 132, 244, 1.000)',
|
||||||
boundsBg: 'rgba(65, 132, 244, 0.100)',
|
boundsBg: 'rgba(65, 132, 244, 0.100)',
|
||||||
|
|
14
types.ts
14
types.ts
|
@ -67,7 +67,11 @@ export enum ShapeType {
|
||||||
// Cubic = "cubic",
|
// Cubic = "cubic",
|
||||||
// Conic = "conic",
|
// Conic = "conic",
|
||||||
|
|
||||||
export type ShapeStyles = Partial<React.SVGProps<SVGUseElement>>
|
export type ShapeStyles = Partial<
|
||||||
|
React.SVGProps<SVGUseElement> & {
|
||||||
|
dash: DashStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
export interface BaseShape {
|
export interface BaseShape {
|
||||||
id: string
|
id: string
|
||||||
|
@ -173,7 +177,13 @@ export interface CodeFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Decoration {
|
export enum Decoration {
|
||||||
Arrow,
|
Arrow = 'Arrow',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum DashStyle {
|
||||||
|
Solid = 'Solid',
|
||||||
|
Dashed = 'Dashed',
|
||||||
|
Dotted = 'Dotted',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ShapeBinding {
|
export interface ShapeBinding {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Bounds } from "types"
|
import { Bounds } from 'types'
|
||||||
import * as vec from "utils/vec"
|
import * as vec from 'utils/vec'
|
||||||
|
import { isAngleBetween } from './utils'
|
||||||
|
|
||||||
interface Intersection {
|
interface Intersection {
|
||||||
didIntersect: boolean
|
didIntersect: boolean
|
||||||
|
@ -26,22 +27,22 @@ export function intersectLineSegments(
|
||||||
const u_b = BV[1] * AV[0] - BV[0] * AV[1]
|
const u_b = BV[1] * AV[0] - BV[0] * AV[1]
|
||||||
|
|
||||||
if (ua_t === 0 || ub_t === 0) {
|
if (ua_t === 0 || ub_t === 0) {
|
||||||
return getIntersection("coincident")
|
return getIntersection('coincident')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (u_b === 0) {
|
if (u_b === 0) {
|
||||||
return getIntersection("parallel")
|
return getIntersection('parallel')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (u_b != 0) {
|
if (u_b != 0) {
|
||||||
const ua = ua_t / u_b
|
const ua = ua_t / u_b
|
||||||
const ub = ub_t / u_b
|
const ub = ub_t / u_b
|
||||||
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
|
if (0 <= ua && ua <= 1 && 0 <= ub && ub <= 1) {
|
||||||
return getIntersection("intersection", vec.add(a1, vec.mul(AV, ua)))
|
return getIntersection('intersection', vec.add(a1, vec.mul(AV, ua)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return getIntersection("no intersection")
|
return getIntersection('no intersection')
|
||||||
}
|
}
|
||||||
|
|
||||||
export function intersectCircleLineSegment(
|
export function intersectCircleLineSegment(
|
||||||
|
@ -65,11 +66,11 @@ export function intersectCircleLineSegment(
|
||||||
const deter = b * b - 4 * a * cc
|
const deter = b * b - 4 * a * cc
|
||||||
|
|
||||||
if (deter < 0) {
|
if (deter < 0) {
|
||||||
return getIntersection("outside")
|
return getIntersection('outside')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deter === 0) {
|
if (deter === 0) {
|
||||||
return getIntersection("tangent")
|
return getIntersection('tangent')
|
||||||
}
|
}
|
||||||
|
|
||||||
var e = Math.sqrt(deter)
|
var e = Math.sqrt(deter)
|
||||||
|
@ -77,9 +78,9 @@ export function intersectCircleLineSegment(
|
||||||
var u2 = (-b - e) / (2 * a)
|
var u2 = (-b - e) / (2 * a)
|
||||||
if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
|
if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
|
||||||
if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
|
if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
|
||||||
return getIntersection("outside")
|
return getIntersection('outside')
|
||||||
} else {
|
} else {
|
||||||
return getIntersection("inside")
|
return getIntersection('inside')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,7 +88,7 @@ export function intersectCircleLineSegment(
|
||||||
if (0 <= u1 && u1 <= 1) results.push(vec.lrp(a1, a2, u1))
|
if (0 <= u1 && u1 <= 1) results.push(vec.lrp(a1, a2, u1))
|
||||||
if (0 <= u2 && u2 <= 1) results.push(vec.lrp(a1, a2, u2))
|
if (0 <= u2 && u2 <= 1) results.push(vec.lrp(a1, a2, u2))
|
||||||
|
|
||||||
return getIntersection("intersection", ...results)
|
return getIntersection('intersection', ...results)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function intersectEllipseLineSegment(
|
export function intersectEllipseLineSegment(
|
||||||
|
@ -100,7 +101,7 @@ export function intersectEllipseLineSegment(
|
||||||
) {
|
) {
|
||||||
// If the ellipse or line segment are empty, return no tValues.
|
// If the ellipse or line segment are empty, return no tValues.
|
||||||
if (rx === 0 || ry === 0 || vec.isEqual(a1, a2)) {
|
if (rx === 0 || ry === 0 || vec.isEqual(a1, a2)) {
|
||||||
return getIntersection("No intersection")
|
return getIntersection('No intersection')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the semimajor and semiminor axes.
|
// Get the semimajor and semiminor axes.
|
||||||
|
@ -141,7 +142,32 @@ export function intersectEllipseLineSegment(
|
||||||
.map((t) => vec.add(center, vec.add(a1, vec.mul(vec.sub(a2, a1), t))))
|
.map((t) => vec.add(center, vec.add(a1, vec.mul(vec.sub(a2, a1), t))))
|
||||||
.map((p) => vec.rotWith(p, center, rotation))
|
.map((p) => vec.rotWith(p, center, rotation))
|
||||||
|
|
||||||
return getIntersection("intersection", ...points)
|
return getIntersection('intersection', ...points)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intersectArcLineSegment(
|
||||||
|
start: number[],
|
||||||
|
end: number[],
|
||||||
|
center: number[],
|
||||||
|
radius: number,
|
||||||
|
A: number[],
|
||||||
|
B: number[]
|
||||||
|
) {
|
||||||
|
const sa = vec.angle(center, start)
|
||||||
|
const ea = vec.angle(center, end)
|
||||||
|
const ellipseTest = intersectEllipseLineSegment(center, radius, radius, A, B)
|
||||||
|
|
||||||
|
if (!ellipseTest.didIntersect) return getIntersection('No intersection')
|
||||||
|
|
||||||
|
const points = ellipseTest.points.filter((point) =>
|
||||||
|
isAngleBetween(sa, ea, vec.angle(center, point))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (points.length === 0) {
|
||||||
|
return getIntersection('No intersection')
|
||||||
|
}
|
||||||
|
|
||||||
|
return getIntersection('intersection', ...points)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function intersectCircleRectangle(
|
export function intersectCircleRectangle(
|
||||||
|
@ -163,19 +189,19 @@ export function intersectCircleRectangle(
|
||||||
const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
|
const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
|
||||||
|
|
||||||
if (topIntersection.didIntersect) {
|
if (topIntersection.didIntersect) {
|
||||||
intersections.push({ ...topIntersection, message: "top" })
|
intersections.push({ ...topIntersection, message: 'top' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightIntersection.didIntersect) {
|
if (rightIntersection.didIntersect) {
|
||||||
intersections.push({ ...rightIntersection, message: "right" })
|
intersections.push({ ...rightIntersection, message: 'right' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bottomIntersection.didIntersect) {
|
if (bottomIntersection.didIntersect) {
|
||||||
intersections.push({ ...bottomIntersection, message: "bottom" })
|
intersections.push({ ...bottomIntersection, message: 'bottom' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leftIntersection.didIntersect) {
|
if (leftIntersection.didIntersect) {
|
||||||
intersections.push({ ...leftIntersection, message: "left" })
|
intersections.push({ ...leftIntersection, message: 'left' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return intersections
|
return intersections
|
||||||
|
@ -230,19 +256,19 @@ export function intersectEllipseRectangle(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (topIntersection.didIntersect) {
|
if (topIntersection.didIntersect) {
|
||||||
intersections.push({ ...topIntersection, message: "top" })
|
intersections.push({ ...topIntersection, message: 'top' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightIntersection.didIntersect) {
|
if (rightIntersection.didIntersect) {
|
||||||
intersections.push({ ...rightIntersection, message: "right" })
|
intersections.push({ ...rightIntersection, message: 'right' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bottomIntersection.didIntersect) {
|
if (bottomIntersection.didIntersect) {
|
||||||
intersections.push({ ...bottomIntersection, message: "bottom" })
|
intersections.push({ ...bottomIntersection, message: 'bottom' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leftIntersection.didIntersect) {
|
if (leftIntersection.didIntersect) {
|
||||||
intersections.push({ ...leftIntersection, message: "left" })
|
intersections.push({ ...leftIntersection, message: 'left' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return intersections
|
return intersections
|
||||||
|
@ -267,19 +293,86 @@ export function intersectRectangleLineSegment(
|
||||||
const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
|
const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
|
||||||
|
|
||||||
if (topIntersection.didIntersect) {
|
if (topIntersection.didIntersect) {
|
||||||
intersections.push({ ...topIntersection, message: "top" })
|
intersections.push({ ...topIntersection, message: 'top' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rightIntersection.didIntersect) {
|
if (rightIntersection.didIntersect) {
|
||||||
intersections.push({ ...rightIntersection, message: "right" })
|
intersections.push({ ...rightIntersection, message: 'right' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bottomIntersection.didIntersect) {
|
if (bottomIntersection.didIntersect) {
|
||||||
intersections.push({ ...bottomIntersection, message: "bottom" })
|
intersections.push({ ...bottomIntersection, message: 'bottom' })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (leftIntersection.didIntersect) {
|
if (leftIntersection.didIntersect) {
|
||||||
intersections.push({ ...leftIntersection, message: "left" })
|
intersections.push({ ...leftIntersection, message: 'left' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return intersections
|
||||||
|
}
|
||||||
|
|
||||||
|
export function intersectArcRectangle(
|
||||||
|
start: number[],
|
||||||
|
end: number[],
|
||||||
|
center: number[],
|
||||||
|
radius: number,
|
||||||
|
point: number[],
|
||||||
|
size: number[]
|
||||||
|
) {
|
||||||
|
const tl = point
|
||||||
|
const tr = vec.add(point, [size[0], 0])
|
||||||
|
const br = vec.add(point, size)
|
||||||
|
const bl = vec.add(point, [0, size[1]])
|
||||||
|
|
||||||
|
const intersections: Intersection[] = []
|
||||||
|
|
||||||
|
const topIntersection = intersectArcLineSegment(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
tl,
|
||||||
|
tr
|
||||||
|
)
|
||||||
|
const rightIntersection = intersectArcLineSegment(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
tr,
|
||||||
|
br
|
||||||
|
)
|
||||||
|
const bottomIntersection = intersectArcLineSegment(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
bl,
|
||||||
|
br
|
||||||
|
)
|
||||||
|
const leftIntersection = intersectArcLineSegment(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
tl,
|
||||||
|
bl
|
||||||
|
)
|
||||||
|
|
||||||
|
if (topIntersection.didIntersect) {
|
||||||
|
intersections.push({ ...topIntersection, message: 'top' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rightIntersection.didIntersect) {
|
||||||
|
intersections.push({ ...rightIntersection, message: 'right' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottomIntersection.didIntersect) {
|
||||||
|
intersections.push({ ...bottomIntersection, message: 'bottom' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leftIntersection.didIntersect) {
|
||||||
|
intersections.push({ ...leftIntersection, message: 'left' })
|
||||||
}
|
}
|
||||||
|
|
||||||
return intersections
|
return intersections
|
||||||
|
@ -360,3 +453,22 @@ export function intersectPolygonBounds(points: number[][], bounds: Bounds) {
|
||||||
|
|
||||||
return intersections
|
return intersections
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function intersectArcBounds(
|
||||||
|
start: number[],
|
||||||
|
end: number[],
|
||||||
|
center: number[],
|
||||||
|
radius: number,
|
||||||
|
bounds: Bounds
|
||||||
|
) {
|
||||||
|
const { minX, minY, width, height } = bounds
|
||||||
|
|
||||||
|
return intersectArcRectangle(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
center,
|
||||||
|
radius,
|
||||||
|
[minX, minY],
|
||||||
|
[width, height]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -1566,3 +1566,18 @@ export function getSvgPathFromStroke(stroke: number[][]) {
|
||||||
d.push('Z')
|
d.push('Z')
|
||||||
return d.join(' ')
|
return d.join(' ')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PI2 = Math.PI * 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is angle c between angles a and b?
|
||||||
|
* @param a
|
||||||
|
* @param b
|
||||||
|
* @param c
|
||||||
|
*/
|
||||||
|
export function isAngleBetween(a: number, b: number, c: number) {
|
||||||
|
if (c === a || c === b) return true
|
||||||
|
const AB = (b - a + PI2) % PI2
|
||||||
|
const AC = (c - a + PI2) % PI2
|
||||||
|
return AB <= Math.PI !== AC > AB
|
||||||
|
}
|
||||||
|
|
15
yarn.lock
15
yarn.lock
|
@ -1283,6 +1283,21 @@
|
||||||
"@radix-ui/react-polymorphic" "0.0.11"
|
"@radix-ui/react-polymorphic" "0.0.11"
|
||||||
"@radix-ui/react-primitive" "0.0.13"
|
"@radix-ui/react-primitive" "0.0.13"
|
||||||
|
|
||||||
|
"@radix-ui/react-checkbox@^0.0.15":
|
||||||
|
version "0.0.15"
|
||||||
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-checkbox/-/react-checkbox-0.0.15.tgz#d53b56854fbba65e74ed4486116107638951b9d1"
|
||||||
|
integrity sha512-R8ErERPlu2kvmqNjxRyyLcS1y3D7J2bQUUEPsvP0BL2AfisUjbT7c9t19k2K/Un3Iieqe93gTPG4LRdbDQQjBw==
|
||||||
|
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-use-controllable-state" "0.0.6"
|
||||||
|
|
||||||
"@radix-ui/react-collection@0.0.12":
|
"@radix-ui/react-collection@0.0.12":
|
||||||
version "0.0.12"
|
version "0.0.12"
|
||||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-0.0.12.tgz#5cd09312cdec34fdbbe1d31affaba69eb768e342"
|
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-0.0.12.tgz#5cd09312cdec34fdbbe1d31affaba69eb768e342"
|
||||||
|
|
Loading…
Reference in a new issue