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]]
|
||||
)
|
||||
|
||||
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 (
|
||||
<g>
|
||||
|
@ -57,7 +57,7 @@ function Handle({
|
|||
pointerEvents="all"
|
||||
transform={`translate(${point})`}
|
||||
>
|
||||
<HandleCircleOuter r={8} />
|
||||
<HandleCircleOuter r={12} />
|
||||
<DotCircle r={4} />
|
||||
</g>
|
||||
)
|
||||
|
|
|
@ -34,10 +34,20 @@ export default function Canvas() {
|
|||
} else {
|
||||
if (isMobile()) {
|
||||
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) => {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
if (inputs.canAccept(e.pointerId)) {
|
||||
|
@ -58,6 +68,7 @@ export default function Canvas() {
|
|||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
onTouchStart={handleTouchStart}
|
||||
// onTouchMove={handleTouchMove}
|
||||
>
|
||||
<Defs />
|
||||
{isReady && (
|
||||
|
|
|
@ -39,7 +39,7 @@ export function ShapeOutline({ id }: { id: string }) {
|
|||
`
|
||||
|
||||
return (
|
||||
<Indicator
|
||||
<SelectIndicator
|
||||
ref={rIndicator}
|
||||
as="use"
|
||||
href={'#' + id}
|
||||
|
@ -50,13 +50,14 @@ export function ShapeOutline({ id }: { id: string }) {
|
|||
)
|
||||
}
|
||||
|
||||
const Indicator = styled('path', {
|
||||
zStrokeWidth: 1,
|
||||
const SelectIndicator = styled('path', {
|
||||
zStrokeWidth: 3,
|
||||
strokeLineCap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: '$selected',
|
||||
fill: 'transparent',
|
||||
pointerEvents: 'all',
|
||||
pointerEvents: 'none',
|
||||
paintOrder: 'stroke fill markers',
|
||||
|
||||
variants: {
|
||||
isLocked: {
|
||||
|
@ -65,5 +66,6 @@ const Indicator = styled('path', {
|
|||
},
|
||||
false: {},
|
||||
},
|
||||
variant: {},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -3,8 +3,9 @@ import { useSelector } from 'state'
|
|||
import styled from 'styles'
|
||||
import { getShapeUtils } from 'lib/shape-utils'
|
||||
import { getPage } from 'utils/utils'
|
||||
import { ShapeStyles } from 'types'
|
||||
import { DashStyle, ShapeStyles } from 'types'
|
||||
import useShapeEvents from 'hooks/useShapeEvents'
|
||||
import { shades, strokes } from 'lib/colors'
|
||||
|
||||
function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
||||
const isHovered = useSelector((state) => state.data.hoveredId === id)
|
||||
|
@ -35,36 +36,61 @@ function Shape({ id, isSelecting }: { id: string; isSelecting: boolean }) {
|
|||
isHovered={isHovered}
|
||||
isSelected={isSelected}
|
||||
transform={transform}
|
||||
{...events}
|
||||
stroke={'red'}
|
||||
strokeWidth={10}
|
||||
>
|
||||
{isSelecting && (
|
||||
<HoverIndicator
|
||||
as="use"
|
||||
href={'#' + id}
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledShape = memo(
|
||||
({ id, style }: { id: string; style: ShapeStyles }) => {
|
||||
return <use href={'#' + id} {...style} />
|
||||
}
|
||||
)
|
||||
const RealShape = memo(({ id, style }: { id: string; style: ShapeStyles }) => {
|
||||
return (
|
||||
<StyledShape
|
||||
as="use"
|
||||
href={'#' + id}
|
||||
{...style}
|
||||
strokeDasharray={getDash(style.dash, +style.strokeWidth)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
||||
const StyledShape = styled('path', {
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
})
|
||||
|
||||
const HoverIndicator = styled('path', {
|
||||
fill: 'none',
|
||||
fill: 'transparent',
|
||||
stroke: 'transparent',
|
||||
pointerEvents: 'all',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
transform: 'all .2s',
|
||||
variants: {
|
||||
variant: {
|
||||
hollow: {
|
||||
pointerEvents: 'stroke',
|
||||
},
|
||||
filled: {
|
||||
pointerEvents: 'all',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StyledGroup = styled('g', {
|
||||
pointerEvents: 'none',
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: '0',
|
||||
},
|
||||
|
@ -84,10 +110,8 @@ const StyledGroup = styled('g', {
|
|||
isHovered: true,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: '1',
|
||||
stroke: '$hint',
|
||||
fill: '$hint',
|
||||
// zStrokeWidth: [8, 4],
|
||||
opacity: '.4',
|
||||
stroke: '$selected',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -96,10 +120,8 @@ const StyledGroup = styled('g', {
|
|||
isHovered: false,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: '1',
|
||||
stroke: '$hint',
|
||||
fill: '$hint',
|
||||
// zStrokeWidth: [6, 3],
|
||||
opacity: '.2',
|
||||
stroke: '$selected',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -108,10 +130,8 @@ const StyledGroup = styled('g', {
|
|||
isHovered: true,
|
||||
css: {
|
||||
[`& ${HoverIndicator}`]: {
|
||||
opacity: '1',
|
||||
stroke: '$hint',
|
||||
fill: '$hint',
|
||||
// zStrokeWidth: [8, 4],
|
||||
opacity: '.2',
|
||||
stroke: '$selected',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -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 default memo(Shape)
|
||||
|
|
|
@ -33,7 +33,7 @@ export default function Editor() {
|
|||
)
|
||||
}
|
||||
|
||||
const Layout = styled('div', {
|
||||
const Layout = styled('main', {
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
|
@ -51,20 +51,24 @@ const Layout = styled('div', {
|
|||
`,
|
||||
})
|
||||
|
||||
const LeftPanels = styled('main', {
|
||||
const LeftPanels = styled('div', {
|
||||
display: 'grid',
|
||||
gridArea: 'leftPanels',
|
||||
gridTemplateRows: '1fr auto',
|
||||
padding: 8,
|
||||
gap: 8,
|
||||
zIndex: 250,
|
||||
pointerEvents: 'none',
|
||||
})
|
||||
|
||||
const RightPanels = styled('main', {
|
||||
const RightPanels = styled('div', {
|
||||
gridArea: 'rightPanels',
|
||||
padding: 8,
|
||||
// display: 'grid',
|
||||
// gridTemplateRows: 'auto',
|
||||
// height: 'fit-content',
|
||||
// justifyContent: 'flex-end',
|
||||
// gap: 8,
|
||||
display: 'grid',
|
||||
gridTemplateRows: 'auto',
|
||||
height: 'fit-content',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 8,
|
||||
zIndex: 300,
|
||||
pointerEvents: 'none',
|
||||
})
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function ColorPicker({ colors, onChange, children }: Props) {
|
|||
{children}
|
||||
<Colors sideOffset={4}>
|
||||
{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} />
|
||||
</ColorButton>
|
||||
))}
|
||||
|
@ -29,7 +29,7 @@ export function ColorIcon({ color }: { color: string }) {
|
|||
)
|
||||
}
|
||||
|
||||
const Colors = styled(DropdownMenu.Content, {
|
||||
export const Colors = styled(DropdownMenu.Content, {
|
||||
display: 'grid',
|
||||
padding: 4,
|
||||
gridTemplateColumns: 'repeat(6, 1fr)',
|
||||
|
@ -117,4 +117,13 @@ export const CurrentColor = styled(DropdownMenu.Trigger, {
|
|||
strokeWidth: 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 { useRef } from 'react'
|
||||
import { IconButton } from 'components/shared'
|
||||
import { Circle, Copy, Lock, Trash, Trash2, Unlock, X } from 'react-feather'
|
||||
import {
|
||||
deepCompare,
|
||||
deepCompareArrays,
|
||||
getPage,
|
||||
getSelectedShapes,
|
||||
} from 'utils/utils'
|
||||
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||
import { Trash2, X } from 'react-feather'
|
||||
import { deepCompare, deepCompareArrays, getPage } from 'utils/utils'
|
||||
import { shades, fills, strokes } from 'lib/colors'
|
||||
|
||||
import ColorPicker, { ColorIcon, CurrentColor } from './color-picker'
|
||||
import AlignDistribute from './align-distribute'
|
||||
import { MoveType, ShapeStyles } from 'types'
|
||||
import WidthPicker from './width-picker'
|
||||
import {
|
||||
AlignTopIcon,
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
AspectRatioIcon,
|
||||
BoxIcon,
|
||||
CheckIcon,
|
||||
CopyIcon,
|
||||
DotsHorizontalIcon,
|
||||
DotsVerticalIcon,
|
||||
EyeClosedIcon,
|
||||
EyeOpenIcon,
|
||||
LockClosedIcon,
|
||||
|
@ -31,11 +26,17 @@ import {
|
|||
PinBottomIcon,
|
||||
PinTopIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
TrashIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import DashPicker from './dash-picker'
|
||||
|
||||
const fillColors = { ...shades, ...fills }
|
||||
const strokeColors = { ...shades, ...strokes }
|
||||
const getFillColor = (color: string) => {
|
||||
if (shades[color]) {
|
||||
return '#fff'
|
||||
}
|
||||
return fillColors[color]
|
||||
}
|
||||
|
||||
export default function StylePanel() {
|
||||
const rContainer = useRef<HTMLDivElement>(null)
|
||||
|
@ -46,14 +47,41 @@ export default function StylePanel() {
|
|||
{isOpen ? (
|
||||
<SelectedShapeStyles />
|
||||
) : (
|
||||
<IconButton onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}>
|
||||
<DotsHorizontalIcon />
|
||||
</IconButton>
|
||||
<>
|
||||
<QuickColorSelect prop="stroke" colors={strokeColors} />
|
||||
<IconButton
|
||||
title="Style"
|
||||
onClick={() => state.send('TOGGLED_STYLE_PANEL_OPEN')}
|
||||
>
|
||||
<DotsVerticalIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</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
|
||||
// information, based on the user's current selection. We might have to keep
|
||||
// track of this data manually within our state.
|
||||
|
@ -79,33 +107,7 @@ function SelectedShapeStyles({}: {}) {
|
|||
return selectedIds.every((id) => page.shapes[id].isHidden)
|
||||
})
|
||||
|
||||
const commonStyle = useSelector((s) => {
|
||||
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 commonStyle = useSelector((s) => s.values.selectedStyle, deepCompare)
|
||||
|
||||
const hasSelection = selectedIds.length > 0
|
||||
|
||||
|
@ -118,28 +120,42 @@ function SelectedShapeStyles({}: {}) {
|
|||
</IconButton>
|
||||
</Panel.Header>
|
||||
<Content>
|
||||
<ColorPicker
|
||||
colors={fillColors}
|
||||
onChange={(color) => state.send('CHANGED_STYLE', { fill: color })}
|
||||
>
|
||||
<CurrentColor>
|
||||
<label>Fill</label>
|
||||
<ColorIcon color={commonStyle.fill} />
|
||||
</CurrentColor>
|
||||
</ColorPicker>
|
||||
<ColorPicker
|
||||
colors={strokeColors}
|
||||
onChange={(color) => state.send('CHANGED_STYLE', { stroke: color })}
|
||||
onChange={(color) =>
|
||||
state.send('CHANGED_STYLE', {
|
||||
stroke: strokeColors[color],
|
||||
fill: getFillColor(color),
|
||||
})
|
||||
}
|
||||
>
|
||||
<CurrentColor>
|
||||
<label>Stroke</label>
|
||||
<label>Color</label>
|
||||
<ColorIcon color={commonStyle.stroke} />
|
||||
</CurrentColor>
|
||||
</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>
|
||||
<label htmlFor="width">Width</label>
|
||||
<WidthPicker strokeWidth={Number(commonStyle.strokeWidth)} />
|
||||
</Row>
|
||||
<Row>
|
||||
<label htmlFor="dash">Dash</label>
|
||||
<DashPicker dash={commonStyle.dash} />
|
||||
</Row>
|
||||
<ButtonsRow>
|
||||
<IconButton
|
||||
disabled={!hasSelection}
|
||||
|
@ -221,14 +237,16 @@ const StylePanelRoot = styled(Panel.Root, {
|
|||
position: 'relative',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '0px 2px 4px rgba(0,0,0,.12)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'all',
|
||||
|
||||
variants: {
|
||||
isOpen: {
|
||||
true: {},
|
||||
false: {
|
||||
padding: 2,
|
||||
height: 38,
|
||||
width: 38,
|
||||
width: 'fit-content',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -275,3 +293,22 @@ const ButtonsRow = styled('div', {
|
|||
justifyContent: 'flex-start',
|
||||
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 { Circle } from 'react-feather'
|
||||
import state from 'state'
|
||||
import styled from 'styles'
|
||||
|
||||
function setWidth(e: ChangeEvent<HTMLInputElement>) {
|
||||
function handleChange(e: ChangeEvent<HTMLInputElement>) {
|
||||
state.send('CHANGED_STYLE', {
|
||||
strokeWidth: Number(e.currentTarget.value),
|
||||
})
|
||||
|
@ -16,7 +15,7 @@ export default function WidthPicker({
|
|||
strokeWidth?: number
|
||||
}) {
|
||||
return (
|
||||
<Group name="width" onValueChange={setWidth}>
|
||||
<Group name="width" onValueChange={handleChange}>
|
||||
<RadioItem value="2" isActive={strokeWidth === 2}>
|
||||
<Circle size={6} />
|
||||
</RadioItem>
|
||||
|
@ -29,52 +28,3 @@ export default function WidthPicker({
|
|||
</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 = {
|
||||
lime: 'rgba(217, 245, 162, 1.000)',
|
||||
green: 'rgba(177, 242, 188, 1.000)',
|
||||
teal: 'rgba(149, 242, 215, 1.000)',
|
||||
cyan: 'rgba(153, 233, 242, 1.000)',
|
||||
blue: 'rgba(166, 216, 255, 1.000)',
|
||||
indigo: 'rgba(186, 200, 255, 1.000)',
|
||||
violet: 'rgba(208, 191, 255, 1.000)',
|
||||
grape: 'rgba(237, 190, 250, 1.000)',
|
||||
pink: 'rgba(252, 194, 215, 1.000)',
|
||||
red: 'rgba(255, 201, 201, 1.000)',
|
||||
orange: 'rgba(255, 216, 168, 1.000)',
|
||||
yellow: 'rgba(255, 236, 153, 1.000)',
|
||||
lime: 'rgba(243, 252, 227, 1.000)',
|
||||
green: 'rgba(235, 251, 238, 1.000)',
|
||||
teal: 'rgba(230, 252, 245, 1.000)',
|
||||
cyan: 'rgba(227, 250, 251, 1.000)',
|
||||
blue: 'rgba(231, 245, 255, 1.000)',
|
||||
indigo: 'rgba(237, 242, 255, 1.000)',
|
||||
violet: 'rgba(242, 240, 255, 1.000)',
|
||||
grape: 'rgba(249, 240, 252, 1.000)',
|
||||
pink: 'rgba(254, 241, 246, 1.000)',
|
||||
red: 'rgba(255, 245, 245, 1.000)',
|
||||
orange: 'rgba(255, 244, 229, 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 { ArrowShape, ShapeHandle, ShapeType } from 'types'
|
||||
import { registerShapeUtils } from './index'
|
||||
import { circleFromThreePoints, clamp, getSweep } from 'utils/utils'
|
||||
import { boundsContained } from 'utils/bounds'
|
||||
import { intersectCircleBounds } from 'utils/intersections'
|
||||
import { circleFromThreePoints, clamp, isAngleBetween } from 'utils/utils'
|
||||
import { pointInBounds } from 'utils/bounds'
|
||||
import {
|
||||
intersectArcBounds,
|
||||
intersectLineSegmentBounds,
|
||||
} from 'utils/intersections'
|
||||
import { getBoundsFromPoints, translateBounds } from 'utils/utils'
|
||||
import { pointInCircle } from 'utils/hitTests'
|
||||
|
||||
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>({
|
||||
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 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 (
|
||||
<g id={id}>
|
||||
|
@ -114,12 +130,14 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
cy={start.point[1]}
|
||||
r={+style.strokeWidth}
|
||||
fill={style.stroke}
|
||||
strokeDasharray="none"
|
||||
/>
|
||||
<polyline
|
||||
points={[b, points[1], c].join()}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
fill="none"
|
||||
strokeDasharray="none"
|
||||
/>
|
||||
</g>
|
||||
)
|
||||
|
@ -127,6 +145,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
|
||||
applyStyles(shape, style) {
|
||||
Object.assign(shape.style, style)
|
||||
shape.style.fill = 'none'
|
||||
return this
|
||||
},
|
||||
|
||||
|
@ -159,24 +178,29 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
)
|
||||
}
|
||||
|
||||
if (!ctpCache.has(shape.handles)) {
|
||||
ctpCache.set(
|
||||
shape.handles,
|
||||
circleFromThreePoints(start.point, end.point, bend.point)
|
||||
)
|
||||
}
|
||||
|
||||
const [cx, cy, r] = ctpCache.get(shape.handles)
|
||||
const [cx, cy, r] = getCtp(shape)
|
||||
|
||||
return !pointInCircle(point, vec.add(shape.point, [cx, cy]), r - 4)
|
||||
},
|
||||
|
||||
hitTestBounds(this, shape, brushBounds) {
|
||||
const shapeBounds = this.getBounds(shape)
|
||||
return (
|
||||
boundsContained(shapeBounds, brushBounds) ||
|
||||
intersectCircleBounds(shape.point, 4, brushBounds).length > 0
|
||||
)
|
||||
const { start, end, bend } = shape.handles
|
||||
|
||||
const sp = vec.add(shape.point, start.point)
|
||||
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) {
|
||||
|
@ -219,14 +243,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
start.point = shape.points[0]
|
||||
end.point = shape.points[1]
|
||||
|
||||
const bendDist = (vec.dist(start.point, end.point) / 2) * shape.bend
|
||||
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
|
||||
bend.point = getBendPoint(shape)
|
||||
|
||||
shape.points = [shape.handles.start.point, shape.handles.end.point]
|
||||
|
||||
|
@ -244,8 +261,6 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
},
|
||||
|
||||
onHandleMove(shape, handles) {
|
||||
const { start, end, bend } = shape.handles
|
||||
|
||||
for (let id in handles) {
|
||||
const handle = handles[id]
|
||||
|
||||
|
@ -255,38 +270,35 @@ const arrow = registerShapeUtils<ArrowShape>({
|
|||
shape.points[handle.index] = handle.point
|
||||
}
|
||||
|
||||
const { start, end, bend } = shape.handles
|
||||
|
||||
const dist = vec.dist(start.point, end.point)
|
||||
|
||||
if (handle.id === 'bend') {
|
||||
const distance = vec.distanceToLineSegment(
|
||||
start.point,
|
||||
end.point,
|
||||
handle.point,
|
||||
true
|
||||
)
|
||||
shape.bend = clamp(distance / (dist / 2), -1, 1)
|
||||
const midPoint = vec.med(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))
|
||||
|
||||
const a0 = vec.angle(handle.point, end.point)
|
||||
const a1 = vec.angle(start.point, end.point)
|
||||
if (a0 - a1 < 0) shape.bend *= -1
|
||||
bend.point = vec.nearestPointOnLineSegment(ap, bp, bend.point, true)
|
||||
shape.bend = vec.dist(bend.point, midPoint) / (dist / 2)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
shape.handles.bend.point =
|
||||
Math.abs(bendDist) > 10
|
||||
? vec.add(midPoint, vec.mul(vec.per(u), bendDist))
|
||||
: midPoint
|
||||
shape.handles.bend.point = getBendPoint(shape)
|
||||
|
||||
return this
|
||||
},
|
||||
|
||||
canTransform: true,
|
||||
canChangeAspectRatio: true,
|
||||
canStyleFill: false,
|
||||
})
|
||||
|
||||
export default arrow
|
||||
|
@ -311,3 +323,16 @@ function getArrowArcPath(
|
|||
end.point[1],
|
||||
].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,
|
||||
canChangeAspectRatio: false,
|
||||
canStyleFill: true,
|
||||
})
|
||||
|
||||
export default circle
|
||||
|
|
|
@ -104,6 +104,7 @@ const dot = registerShapeUtils<DotShape>({
|
|||
|
||||
canTransform: false,
|
||||
canChangeAspectRatio: false,
|
||||
canStyleFill: true,
|
||||
})
|
||||
|
||||
export default dot
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
getSvgPathFromStroke,
|
||||
translateBounds,
|
||||
} from 'utils/utils'
|
||||
import styled from 'styles'
|
||||
|
||||
const pathCache = new WeakMap<DrawShape['points'], string>([])
|
||||
|
||||
|
@ -190,6 +191,11 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
|
||||
canTransform: true,
|
||||
canChangeAspectRatio: true,
|
||||
canStyleFill: false,
|
||||
})
|
||||
|
||||
export default draw
|
||||
|
||||
const DrawPath = styled('path', {
|
||||
strokeWidth: 0,
|
||||
})
|
||||
|
|
|
@ -149,6 +149,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
|||
|
||||
canTransform: true,
|
||||
canChangeAspectRatio: true,
|
||||
canStyleFill: true,
|
||||
})
|
||||
|
||||
export default ellipse
|
||||
|
|
|
@ -39,6 +39,9 @@ export interface ShapeUtility<K extends Readonly<Shape>> {
|
|||
// Whether the shape's aspect ratio can change
|
||||
canChangeAspectRatio: boolean
|
||||
|
||||
// Whether the shape's style can be filled
|
||||
canStyleFill: boolean
|
||||
|
||||
// Create a new shape.
|
||||
create(props: Partial<K>): K
|
||||
|
||||
|
|
|
@ -113,6 +113,7 @@ const line = registerShapeUtils<LineShape>({
|
|||
|
||||
canTransform: false,
|
||||
canChangeAspectRatio: false,
|
||||
canStyleFill: false,
|
||||
})
|
||||
|
||||
export default line
|
||||
|
|
|
@ -137,6 +137,7 @@ const polyline = registerShapeUtils<PolylineShape>({
|
|||
|
||||
canTransform: true,
|
||||
canChangeAspectRatio: true,
|
||||
canStyleFill: false,
|
||||
})
|
||||
|
||||
export default polyline
|
||||
|
|
|
@ -112,6 +112,7 @@ const ray = registerShapeUtils<RayShape>({
|
|||
|
||||
canTransform: false,
|
||||
canChangeAspectRatio: false,
|
||||
canStyleFill: false,
|
||||
})
|
||||
|
||||
export default ray
|
||||
|
|
|
@ -150,6 +150,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
|||
|
||||
canTransform: true,
|
||||
canChangeAspectRatio: true,
|
||||
canStyleFill: true,
|
||||
})
|
||||
|
||||
export default rectangle
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.1.3",
|
||||
"@radix-ui/react-checkbox": "^0.0.15",
|
||||
"@radix-ui/react-dropdown-menu": "^0.0.19",
|
||||
"@radix-ui/react-icons": "^1.0.3",
|
||||
"@radix-ui/react-radio-group": "^0.0.16",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react'
|
||||
import { PointerInfo } from 'types'
|
||||
import { isDarwin } from 'utils/utils'
|
||||
|
||||
|
@ -5,6 +6,52 @@ class Inputs {
|
|||
activePointerId?: number
|
||||
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) {
|
||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
getCurrent,
|
||||
getPage,
|
||||
getSelectedBounds,
|
||||
getSelectedShapes,
|
||||
getShape,
|
||||
screenToWorld,
|
||||
setZoomCSS,
|
||||
|
@ -32,6 +33,7 @@ import {
|
|||
DistributeType,
|
||||
AlignType,
|
||||
StretchType,
|
||||
DashStyle,
|
||||
} from 'types'
|
||||
|
||||
const initialData: Data = {
|
||||
|
@ -50,6 +52,7 @@ const initialData: Data = {
|
|||
fill: shades.lightGray,
|
||||
stroke: shades.darkGray,
|
||||
strokeWidth: 2,
|
||||
dash: DashStyle.Solid,
|
||||
},
|
||||
camera: {
|
||||
point: [0, 0],
|
||||
|
@ -1296,6 +1299,35 @@ const state = createState({
|
|||
...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: {
|
||||
brushFill: 'rgba(0,0,0,.1)',
|
||||
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)',
|
||||
bounds: 'rgba(65, 132, 244, 1.000)',
|
||||
boundsBg: 'rgba(65, 132, 244, 0.100)',
|
||||
|
|
14
types.ts
14
types.ts
|
@ -67,7 +67,11 @@ export enum ShapeType {
|
|||
// Cubic = "cubic",
|
||||
// Conic = "conic",
|
||||
|
||||
export type ShapeStyles = Partial<React.SVGProps<SVGUseElement>>
|
||||
export type ShapeStyles = Partial<
|
||||
React.SVGProps<SVGUseElement> & {
|
||||
dash: DashStyle
|
||||
}
|
||||
>
|
||||
|
||||
export interface BaseShape {
|
||||
id: string
|
||||
|
@ -173,7 +177,13 @@ export interface CodeFile {
|
|||
}
|
||||
|
||||
export enum Decoration {
|
||||
Arrow,
|
||||
Arrow = 'Arrow',
|
||||
}
|
||||
|
||||
export enum DashStyle {
|
||||
Solid = 'Solid',
|
||||
Dashed = 'Dashed',
|
||||
Dotted = 'Dotted',
|
||||
}
|
||||
|
||||
export interface ShapeBinding {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Bounds } from "types"
|
||||
import * as vec from "utils/vec"
|
||||
import { Bounds } from 'types'
|
||||
import * as vec from 'utils/vec'
|
||||
import { isAngleBetween } from './utils'
|
||||
|
||||
interface Intersection {
|
||||
didIntersect: boolean
|
||||
|
@ -26,22 +27,22 @@ export function intersectLineSegments(
|
|||
const u_b = BV[1] * AV[0] - BV[0] * AV[1]
|
||||
|
||||
if (ua_t === 0 || ub_t === 0) {
|
||||
return getIntersection("coincident")
|
||||
return getIntersection('coincident')
|
||||
}
|
||||
|
||||
if (u_b === 0) {
|
||||
return getIntersection("parallel")
|
||||
return getIntersection('parallel')
|
||||
}
|
||||
|
||||
if (u_b != 0) {
|
||||
const ua = ua_t / u_b
|
||||
const ub = ub_t / u_b
|
||||
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(
|
||||
|
@ -65,11 +66,11 @@ export function intersectCircleLineSegment(
|
|||
const deter = b * b - 4 * a * cc
|
||||
|
||||
if (deter < 0) {
|
||||
return getIntersection("outside")
|
||||
return getIntersection('outside')
|
||||
}
|
||||
|
||||
if (deter === 0) {
|
||||
return getIntersection("tangent")
|
||||
return getIntersection('tangent')
|
||||
}
|
||||
|
||||
var e = Math.sqrt(deter)
|
||||
|
@ -77,9 +78,9 @@ export function intersectCircleLineSegment(
|
|||
var u2 = (-b - e) / (2 * a)
|
||||
if ((u1 < 0 || u1 > 1) && (u2 < 0 || u2 > 1)) {
|
||||
if ((u1 < 0 && u2 < 0) || (u1 > 1 && u2 > 1)) {
|
||||
return getIntersection("outside")
|
||||
return getIntersection('outside')
|
||||
} 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 <= u2 && u2 <= 1) results.push(vec.lrp(a1, a2, u2))
|
||||
|
||||
return getIntersection("intersection", ...results)
|
||||
return getIntersection('intersection', ...results)
|
||||
}
|
||||
|
||||
export function intersectEllipseLineSegment(
|
||||
|
@ -100,7 +101,7 @@ export function intersectEllipseLineSegment(
|
|||
) {
|
||||
// If the ellipse or line segment are empty, return no tValues.
|
||||
if (rx === 0 || ry === 0 || vec.isEqual(a1, a2)) {
|
||||
return getIntersection("No intersection")
|
||||
return getIntersection('No intersection')
|
||||
}
|
||||
|
||||
// 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((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(
|
||||
|
@ -163,19 +189,19 @@ export function intersectCircleRectangle(
|
|||
const leftIntersection = intersectCircleLineSegment(c, r, tl, bl)
|
||||
|
||||
if (topIntersection.didIntersect) {
|
||||
intersections.push({ ...topIntersection, message: "top" })
|
||||
intersections.push({ ...topIntersection, message: 'top' })
|
||||
}
|
||||
|
||||
if (rightIntersection.didIntersect) {
|
||||
intersections.push({ ...rightIntersection, message: "right" })
|
||||
intersections.push({ ...rightIntersection, message: 'right' })
|
||||
}
|
||||
|
||||
if (bottomIntersection.didIntersect) {
|
||||
intersections.push({ ...bottomIntersection, message: "bottom" })
|
||||
intersections.push({ ...bottomIntersection, message: 'bottom' })
|
||||
}
|
||||
|
||||
if (leftIntersection.didIntersect) {
|
||||
intersections.push({ ...leftIntersection, message: "left" })
|
||||
intersections.push({ ...leftIntersection, message: 'left' })
|
||||
}
|
||||
|
||||
return intersections
|
||||
|
@ -230,19 +256,19 @@ export function intersectEllipseRectangle(
|
|||
)
|
||||
|
||||
if (topIntersection.didIntersect) {
|
||||
intersections.push({ ...topIntersection, message: "top" })
|
||||
intersections.push({ ...topIntersection, message: 'top' })
|
||||
}
|
||||
|
||||
if (rightIntersection.didIntersect) {
|
||||
intersections.push({ ...rightIntersection, message: "right" })
|
||||
intersections.push({ ...rightIntersection, message: 'right' })
|
||||
}
|
||||
|
||||
if (bottomIntersection.didIntersect) {
|
||||
intersections.push({ ...bottomIntersection, message: "bottom" })
|
||||
intersections.push({ ...bottomIntersection, message: 'bottom' })
|
||||
}
|
||||
|
||||
if (leftIntersection.didIntersect) {
|
||||
intersections.push({ ...leftIntersection, message: "left" })
|
||||
intersections.push({ ...leftIntersection, message: 'left' })
|
||||
}
|
||||
|
||||
return intersections
|
||||
|
@ -267,19 +293,86 @@ export function intersectRectangleLineSegment(
|
|||
const leftIntersection = intersectLineSegments(a1, a2, tl, bl)
|
||||
|
||||
if (topIntersection.didIntersect) {
|
||||
intersections.push({ ...topIntersection, message: "top" })
|
||||
intersections.push({ ...topIntersection, message: 'top' })
|
||||
}
|
||||
|
||||
if (rightIntersection.didIntersect) {
|
||||
intersections.push({ ...rightIntersection, message: "right" })
|
||||
intersections.push({ ...rightIntersection, message: 'right' })
|
||||
}
|
||||
|
||||
if (bottomIntersection.didIntersect) {
|
||||
intersections.push({ ...bottomIntersection, message: "bottom" })
|
||||
intersections.push({ ...bottomIntersection, message: 'bottom' })
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -360,3 +453,22 @@ export function intersectPolygonBounds(points: number[][], bounds: Bounds) {
|
|||
|
||||
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')
|
||||
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-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":
|
||||
version "0.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-0.0.12.tgz#5cd09312cdec34fdbbe1d31affaba69eb768e342"
|
||||
|
|
Loading…
Reference in a new issue