diff --git a/components/canvas/bounds/handles.tsx b/components/canvas/bounds/handles.tsx
index f59b2e43f..037ee9cf9 100644
--- a/components/canvas/bounds/handles.tsx
+++ b/components/canvas/bounds/handles.tsx
@@ -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 (
@@ -57,7 +57,7 @@ function Handle({
pointerEvents="all"
transform={`translate(${point})`}
>
-
+
)
diff --git a/components/canvas/canvas.tsx b/components/canvas/canvas.tsx
index 0a080fd32..20162273b 100644
--- a/components/canvas/canvas.tsx
+++ b/components/canvas/canvas.tsx
@@ -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}
>
{isReady && (
diff --git a/components/canvas/selected.tsx b/components/canvas/selected.tsx
index ba7251583..a7955d2b4 100644
--- a/components/canvas/selected.tsx
+++ b/components/canvas/selected.tsx
@@ -39,7 +39,7 @@ export function ShapeOutline({ id }: { id: string }) {
`
return (
- 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 && (
)}
- {!shape.isHidden && }
+ {!shape.isHidden && (
+
+ )}
)
}
-const StyledShape = memo(
- ({ id, style }: { id: string; style: ShapeStyles }) => {
- return
- }
-)
+const RealShape = memo(({ id, style }: { id: string; style: ShapeStyles }) => {
+ return (
+
+ )
+})
+
+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)
diff --git a/components/editor.tsx b/components/editor.tsx
index 42cde2330..33a65e30b 100644
--- a/components/editor.tsx
+++ b/components/editor.tsx
@@ -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',
})
diff --git a/components/style-panel/color-picker.tsx b/components/style-panel/color-picker.tsx
index d64cec2c0..e9f5dc57e 100644
--- a/components/style-panel/color-picker.tsx
+++ b/components/style-panel/color-picker.tsx
@@ -14,7 +14,7 @@ export default function ColorPicker({ colors, onChange, children }: Props) {
{children}
{Object.entries(colors).map(([name, color]) => (
- onChange(color)}>
+ onChange(name)}>
))}
@@ -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',
+ },
+ },
+ },
})
diff --git a/components/style-panel/dash-picker.tsx b/components/style-panel/dash-picker.tsx
new file mode 100644
index 000000000..af8810a3e
--- /dev/null
+++ b/components/style-panel/dash-picker.tsx
@@ -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) {
+ state.send('CHANGED_STYLE', {
+ dash: e.currentTarget.value,
+ })
+}
+
+interface Props {
+ dash: DashStyle
+}
+
+export default function DashPicker({ dash }: Props) {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+function DashSolidIcon() {
+ return (
+
+ )
+}
+
+function DashDashedIcon() {
+ return (
+
+ )
+}
+
+function DashDottedIcon() {
+ return (
+
+ )
+}
diff --git a/components/style-panel/shared.tsx b/components/style-panel/shared.tsx
new file mode 100644
index 000000000..9543d3c4f
--- /dev/null
+++ b/components/style-panel/shared.tsx
@@ -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',
+ },
+ },
+ },
+ },
+})
diff --git a/components/style-panel/style-panel.tsx b/components/style-panel/style-panel.tsx
index fc2970125..0098c2fa0 100644
--- a/components/style-panel/style-panel.tsx
+++ b/components/style-panel/style-panel.tsx
@@ -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(null)
@@ -46,14 +47,41 @@ export default function StylePanel() {
{isOpen ? (
) : (
- state.send('TOGGLED_STYLE_PANEL_OPEN')}>
-
-
+ <>
+
+ state.send('TOGGLED_STYLE_PANEL_OPEN')}
+ >
+
+
+ >
)}
)
}
+function QuickColorSelect({
+ prop,
+ colors,
+}: {
+ prop: ShapeStyles['fill'] | ShapeStyles['stroke']
+ colors: Record
+}) {
+ const value = useSelector((s) => s.values.selectedStyle[prop])
+
+ return (
+ state.send('CHANGED_STYLE', { [prop]: color })}
+ >
+
+
+
+
+ )
+}
+
// 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 = {}
- const overrides = new Set([])
-
- 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({}: {}) {
- state.send('CHANGED_STYLE', { fill: color })}
- >
-
-
-
-
-
state.send('CHANGED_STYLE', { stroke: color })}
+ onChange={(color) =>
+ state.send('CHANGED_STYLE', {
+ stroke: strokeColors[color],
+ fill: getFillColor(color),
+ })
+ }
>
-
+
+ {/*
+
+ ) => {
+ console.log(e.target.value)
+ state.send('CHANGED_STYLE', {
+ isFilled: e.target.value === 'on',
+ })
+ }}
+ >
+
+
+
*/}
+
+
+
+
) {
+function handleChange(e: ChangeEvent) {
state.send('CHANGED_STYLE', {
strokeWidth: Number(e.currentTarget.value),
})
@@ -16,7 +15,7 @@ export default function WidthPicker({
strokeWidth?: number
}) {
return (
-
+
@@ -29,52 +28,3 @@ export default function WidthPicker({
)
}
-
-const Group = styled(RadioGroup.Root, {
- display: 'flex',
-})
-
-const RadioItem = styled(RadioGroup.Item, {
- height: '32px',
- width: '32px',
- backgroundColor: '$panel',
- borderRadius: '4px',
- padding: '0',
- margin: '0',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- outline: 'none',
- border: 'none',
- pointerEvents: 'all',
- cursor: 'pointer',
-
- '&:hover:not(:disabled)': {
- backgroundColor: '$hover',
- '& svg': {
- fill: '$text',
- strokeWidth: '0',
- },
- },
-
- '&:disabled': {
- opacity: '0.5',
- },
-
- variants: {
- isActive: {
- true: {
- '& svg': {
- fill: '$text',
- strokeWidth: '0',
- },
- },
- false: {
- '& svg': {
- fill: '$inactive',
- strokeWidth: '0',
- },
- },
- },
- },
-})
diff --git a/lib/colors.ts b/lib/colors.ts
index 611a238f4..391c74837 100644
--- a/lib/colors.ts
+++ b/lib/colors.ts
@@ -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)',
}
diff --git a/lib/shape-utils/arrow.tsx b/lib/shape-utils/arrow.tsx
index a99016810..0fcd87b64 100644
--- a/lib/shape-utils/arrow.tsx
+++ b/lib/shape-utils/arrow.tsx
@@ -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()
+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({
boundsCache: new WeakMap([]),
@@ -69,7 +84,8 @@ const arrow = registerShapeUtils({
}
},
- 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({
)
}
- const circle = showCircle && ctpCache.get(handles)
+ const circle = showCircle && getCtp(shape)
return (
@@ -114,12 +130,14 @@ const arrow = registerShapeUtils({
cy={start.point[1]}
r={+style.strokeWidth}
fill={style.stroke}
+ strokeDasharray="none"
/>
)
@@ -127,6 +145,7 @@ const arrow = registerShapeUtils({
applyStyles(shape, style) {
Object.assign(shape.style, style)
+ shape.style.fill = 'none'
return this
},
@@ -159,24 +178,29 @@ const arrow = registerShapeUtils({
)
}
- 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({
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({
},
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({
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))
+}
diff --git a/lib/shape-utils/circle.tsx b/lib/shape-utils/circle.tsx
index 37501eb99..a4dc0760f 100644
--- a/lib/shape-utils/circle.tsx
+++ b/lib/shape-utils/circle.tsx
@@ -135,6 +135,7 @@ const circle = registerShapeUtils({
canTransform: true,
canChangeAspectRatio: false,
+ canStyleFill: true,
})
export default circle
diff --git a/lib/shape-utils/dot.tsx b/lib/shape-utils/dot.tsx
index 7f01be34a..eceafa6ef 100644
--- a/lib/shape-utils/dot.tsx
+++ b/lib/shape-utils/dot.tsx
@@ -104,6 +104,7 @@ const dot = registerShapeUtils({
canTransform: false,
canChangeAspectRatio: false,
+ canStyleFill: true,
})
export default dot
diff --git a/lib/shape-utils/draw.tsx b/lib/shape-utils/draw.tsx
index beb7add32..e46185150 100644
--- a/lib/shape-utils/draw.tsx
+++ b/lib/shape-utils/draw.tsx
@@ -11,6 +11,7 @@ import {
getSvgPathFromStroke,
translateBounds,
} from 'utils/utils'
+import styled from 'styles'
const pathCache = new WeakMap([])
@@ -190,6 +191,11 @@ const draw = registerShapeUtils({
canTransform: true,
canChangeAspectRatio: true,
+ canStyleFill: false,
})
export default draw
+
+const DrawPath = styled('path', {
+ strokeWidth: 0,
+})
diff --git a/lib/shape-utils/ellipse.tsx b/lib/shape-utils/ellipse.tsx
index 0d1c6421d..43f0fac39 100644
--- a/lib/shape-utils/ellipse.tsx
+++ b/lib/shape-utils/ellipse.tsx
@@ -149,6 +149,7 @@ const ellipse = registerShapeUtils({
canTransform: true,
canChangeAspectRatio: true,
+ canStyleFill: true,
})
export default ellipse
diff --git a/lib/shape-utils/index.tsx b/lib/shape-utils/index.tsx
index b96d81812..5ab8125ef 100644
--- a/lib/shape-utils/index.tsx
+++ b/lib/shape-utils/index.tsx
@@ -39,6 +39,9 @@ export interface ShapeUtility> {
// 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
diff --git a/lib/shape-utils/line.tsx b/lib/shape-utils/line.tsx
index 56a618c9d..a4e34ff48 100644
--- a/lib/shape-utils/line.tsx
+++ b/lib/shape-utils/line.tsx
@@ -113,6 +113,7 @@ const line = registerShapeUtils({
canTransform: false,
canChangeAspectRatio: false,
+ canStyleFill: false,
})
export default line
diff --git a/lib/shape-utils/polyline.tsx b/lib/shape-utils/polyline.tsx
index 14352e5a3..411d550c6 100644
--- a/lib/shape-utils/polyline.tsx
+++ b/lib/shape-utils/polyline.tsx
@@ -137,6 +137,7 @@ const polyline = registerShapeUtils({
canTransform: true,
canChangeAspectRatio: true,
+ canStyleFill: false,
})
export default polyline
diff --git a/lib/shape-utils/ray.tsx b/lib/shape-utils/ray.tsx
index 43889b045..e9938bfa0 100644
--- a/lib/shape-utils/ray.tsx
+++ b/lib/shape-utils/ray.tsx
@@ -112,6 +112,7 @@ const ray = registerShapeUtils({
canTransform: false,
canChangeAspectRatio: false,
+ canStyleFill: false,
})
export default ray
diff --git a/lib/shape-utils/rectangle.tsx b/lib/shape-utils/rectangle.tsx
index bafd5f46e..8e8421686 100644
--- a/lib/shape-utils/rectangle.tsx
+++ b/lib/shape-utils/rectangle.tsx
@@ -150,6 +150,7 @@ const rectangle = registerShapeUtils({
canTransform: true,
canChangeAspectRatio: true,
+ canStyleFill: true,
})
export default rectangle
diff --git a/package.json b/package.json
index 9bc4aa916..22c8e5665 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/state/inputs.tsx b/state/inputs.tsx
index 28bf6ebdb..91ad32f0e 100644
--- a/state/inputs.tsx
+++ b/state/inputs.tsx
@@ -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 = {}
+ 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
diff --git a/state/state.ts b/state/state.ts
index abc3ca534..84c0a7bd3 100644
--- a/state/state.ts
+++ b/state/state.ts
@@ -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 = {}
+
+ const overrides = new Set([])
+
+ 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
+ },
},
})
diff --git a/styles/stitches.config.ts b/styles/stitches.config.ts
index cb98749c2..c561ed848 100644
--- a/styles/stitches.config.ts
+++ b/styles/stitches.config.ts
@@ -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)',
diff --git a/types.ts b/types.ts
index 73a155c2e..d148bac5e 100644
--- a/types.ts
+++ b/types.ts
@@ -67,7 +67,11 @@ export enum ShapeType {
// Cubic = "cubic",
// Conic = "conic",
-export type ShapeStyles = Partial>
+export type ShapeStyles = Partial<
+ React.SVGProps & {
+ 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 {
diff --git a/utils/intersections.ts b/utils/intersections.ts
index 553fcc646..e115095ee 100644
--- a/utils/intersections.ts
+++ b/utils/intersections.ts
@@ -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]
+ )
+}
diff --git a/utils/utils.ts b/utils/utils.ts
index 47959cab1..41c4b1045 100644
--- a/utils/utils.ts
+++ b/utils/utils.ts
@@ -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
+}
diff --git a/yarn.lock b/yarn.lock
index 66a52a5be..6517af66a 100644
--- a/yarn.lock
+++ b/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"