Adds most of text feature, except creation
This commit is contained in:
parent
94fcca1685
commit
027815f199
40 changed files with 718 additions and 124 deletions
|
@ -15,7 +15,6 @@ import CenterHandle from './center-handle'
|
||||||
import CornerHandle from './corner-handle'
|
import CornerHandle from './corner-handle'
|
||||||
import EdgeHandle from './edge-handle'
|
import EdgeHandle from './edge-handle'
|
||||||
import RotateHandle from './rotate-handle'
|
import RotateHandle from './rotate-handle'
|
||||||
import Handles from './handles'
|
|
||||||
|
|
||||||
export default function Bounds() {
|
export default function Bounds() {
|
||||||
const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
|
const isBrushing = useSelector((s) => s.isIn('brushSelecting'))
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useCallback, useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import inputs from 'state/inputs'
|
import inputs from 'state/inputs'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
|
@ -8,11 +8,12 @@ function handlePointerDown(e: React.PointerEvent<SVGRectElement>) {
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.currentTarget.setPointerCapture(e.pointerId)
|
e.currentTarget.setPointerCapture(e.pointerId)
|
||||||
|
const info = inputs.pointerDown(e, 'bounds')
|
||||||
|
|
||||||
if (e.button === 0) {
|
if (e.button === 0) {
|
||||||
state.send('POINTED_BOUNDS', inputs.pointerDown(e, 'bounds'))
|
state.send('POINTED_BOUNDS', info)
|
||||||
} else if (e.button === 2) {
|
} else if (e.button === 2) {
|
||||||
state.send('RIGHT_POINTED', inputs.pointerDown(e, 'bounds'))
|
state.send('RIGHT_POINTED', info)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,12 +23,12 @@ export default function CenterHandle({
|
||||||
const StyledBounds = styled('rect', {
|
const StyledBounds = styled('rect', {
|
||||||
fill: 'none',
|
fill: 'none',
|
||||||
stroke: '$bounds',
|
stroke: '$bounds',
|
||||||
zStrokeWidth: 2,
|
zStrokeWidth: 1.5,
|
||||||
|
|
||||||
variants: {
|
variants: {
|
||||||
isLocked: {
|
isLocked: {
|
||||||
true: {
|
true: {
|
||||||
zStrokeWidth: 1,
|
zStrokeWidth: 1.5,
|
||||||
zDash: 2,
|
zDash: 2,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import useHandleEvents from 'hooks/useBoundsHandleEvents'
|
import useBoundsEvents from 'hooks/useBoundsEvents'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { Corner, Bounds } from 'types'
|
import { Corner, Bounds } from 'types'
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ export default function CornerHandle({
|
||||||
bounds: Bounds
|
bounds: Bounds
|
||||||
corner: Corner
|
corner: Corner
|
||||||
}) {
|
}) {
|
||||||
const events = useHandleEvents(corner)
|
const events = useBoundsEvents(corner)
|
||||||
|
|
||||||
const isTop = corner === Corner.TopLeft || corner === Corner.TopRight
|
const isTop = corner === Corner.TopLeft || corner === Corner.TopRight
|
||||||
const isLeft = corner === Corner.TopLeft || corner === Corner.BottomLeft
|
const isLeft = corner === Corner.TopLeft || corner === Corner.BottomLeft
|
||||||
|
@ -53,5 +53,5 @@ const StyledCorner = styled('rect', {
|
||||||
const StyledCornerInner = styled('rect', {
|
const StyledCornerInner = styled('rect', {
|
||||||
stroke: '$bounds',
|
stroke: '$bounds',
|
||||||
fill: '#fff',
|
fill: '#fff',
|
||||||
zStrokeWidth: 2,
|
zStrokeWidth: 1.5,
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import useHandleEvents from 'hooks/useBoundsHandleEvents'
|
import useBoundsEvents from 'hooks/useBoundsEvents'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { Edge, Bounds } from 'types'
|
import { Edge, Bounds } from 'types'
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ export default function EdgeHandle({
|
||||||
bounds: Bounds
|
bounds: Bounds
|
||||||
edge: Edge
|
edge: Edge
|
||||||
}) {
|
}) {
|
||||||
const events = useHandleEvents(edge)
|
const events = useBoundsEvents(edge)
|
||||||
|
|
||||||
const isHorizontal = edge === Edge.Top || edge === Edge.Bottom
|
const isHorizontal = edge === Edge.Top || edge === Edge.Bottom
|
||||||
const isFarEdge = edge === Edge.Right || edge === Edge.Bottom
|
const isFarEdge = edge === Edge.Right || edge === Edge.Bottom
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import useHandleEvents from 'hooks/useBoundsHandleEvents'
|
import useHandleEvents from 'hooks/useBoundsEvents'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { Bounds } from 'types'
|
import { Bounds } from 'types'
|
||||||
|
|
||||||
|
@ -33,6 +33,6 @@ export default function Rotate({
|
||||||
const StyledRotateHandle = styled('circle', {
|
const StyledRotateHandle = styled('circle', {
|
||||||
stroke: '$bounds',
|
stroke: '$bounds',
|
||||||
fill: '#fff',
|
fill: '#fff',
|
||||||
zStrokeWidth: 2,
|
zStrokeWidth: 1.5,
|
||||||
cursor: 'grab',
|
cursor: 'grab',
|
||||||
})
|
})
|
||||||
|
|
|
@ -9,6 +9,8 @@ export default function Defs() {
|
||||||
|
|
||||||
const currentPageShapeIds = useSelector(({ data }) => {
|
const currentPageShapeIds = useSelector(({ data }) => {
|
||||||
return Object.values(getPage(data).shapes)
|
return Object.values(getPage(data).shapes)
|
||||||
|
.filter(Boolean)
|
||||||
|
.filter((shape) => !getShapeUtils(shape).isForeignObject)
|
||||||
.sort((a, b) => a.childIndex - b.childIndex)
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
.map((shape) => shape.id)
|
.map((shape) => shape.id)
|
||||||
}, deepCompareArrays)
|
}, deepCompareArrays)
|
||||||
|
@ -29,6 +31,7 @@ export default function Defs() {
|
||||||
|
|
||||||
const Def = memo(function Def({ id }: { id: string }) {
|
const Def = memo(function Def({ id }: { id: string }) {
|
||||||
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
||||||
|
|
||||||
if (!shape) return null
|
if (!shape) return null
|
||||||
return getShapeUtils(shape).render(shape)
|
return getShapeUtils(shape).render(shape, { isEditing: false })
|
||||||
})
|
})
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default function Page() {
|
||||||
[window.innerWidth, window.innerHeight],
|
[window.innerWidth, window.innerHeight],
|
||||||
s.data
|
s.data
|
||||||
)
|
)
|
||||||
|
|
||||||
viewportCache.set(pageState, {
|
viewportCache.set(pageState, {
|
||||||
minX,
|
minX,
|
||||||
minY,
|
minY,
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useRef, memo } from 'react'
|
import React, { useRef, memo } from 'react'
|
||||||
import { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import { getShapeUtils } from 'lib/shape-utils'
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
import { getBoundsCenter, getPage } from 'utils/utils'
|
import { getBoundsCenter, getPage } from 'utils/utils'
|
||||||
|
@ -18,9 +18,11 @@ interface ShapeProps {
|
||||||
function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
||||||
|
|
||||||
|
const isEditing = useSelector((s) => s.data.editingId === id)
|
||||||
|
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
|
||||||
const events = useShapeEvents(id, shape?.type === ShapeType.Group, rGroup)
|
const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
|
||||||
|
|
||||||
// This is a problem with deleted shapes. The hooks in this component
|
// This is a problem with deleted shapes. The hooks in this component
|
||||||
// may sometimes run before the hook in the Page component, which means
|
// may sometimes run before the hook in the Page component, which means
|
||||||
|
@ -28,9 +30,13 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
// detects the change and pulls this component.
|
// detects the change and pulls this component.
|
||||||
if (!shape) return null
|
if (!shape) return null
|
||||||
|
|
||||||
const isGroup = shape.type === ShapeType.Group
|
const utils = getShapeUtils(shape)
|
||||||
|
const style = getShapeStyle(shape.style)
|
||||||
|
const shapeUtils = getShapeUtils(shape)
|
||||||
|
const { isShy, isParent, isForeignObject } = shapeUtils
|
||||||
|
|
||||||
const center = getShapeUtils(shape).getCenter(shape)
|
const bounds = shapeUtils.getBounds(shape)
|
||||||
|
const center = shapeUtils.getCenter(shape)
|
||||||
const rotation = shape.rotation * (180 / Math.PI)
|
const rotation = shape.rotation * (180 / Math.PI)
|
||||||
|
|
||||||
const transform = `
|
const transform = `
|
||||||
|
@ -39,11 +45,24 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
translate(${shape.point})
|
translate(${shape.point})
|
||||||
`
|
`
|
||||||
|
|
||||||
const style = getShapeStyle(shape.style)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledGroup ref={rGroup} transform={transform}>
|
<StyledGroup
|
||||||
{isSelecting && !isGroup && (
|
ref={rGroup}
|
||||||
|
transform={transform}
|
||||||
|
onBlur={() => state.send('BLURRED_SHAPE', { target: id })}
|
||||||
|
>
|
||||||
|
{isSelecting &&
|
||||||
|
!isShy &&
|
||||||
|
(isForeignObject ? (
|
||||||
|
<HoverIndicator
|
||||||
|
as="rect"
|
||||||
|
width={bounds.width}
|
||||||
|
height={bounds.height}
|
||||||
|
strokeWidth={1.5}
|
||||||
|
variant={'ghost'}
|
||||||
|
{...events}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<HoverIndicator
|
<HoverIndicator
|
||||||
as="use"
|
as="use"
|
||||||
href={'#' + id}
|
href={'#' + id}
|
||||||
|
@ -51,10 +70,21 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
variant={getShapeUtils(shape).canStyleFill ? 'filled' : 'hollow'}
|
variant={getShapeUtils(shape).canStyleFill ? 'filled' : 'hollow'}
|
||||||
{...events}
|
{...events}
|
||||||
/>
|
/>
|
||||||
)}
|
))}
|
||||||
|
|
||||||
{!shape.isHidden && <RealShape isGroup={isGroup} id={id} style={style} />}
|
{!shape.isHidden &&
|
||||||
{isGroup &&
|
(isForeignObject ? (
|
||||||
|
shapeUtils.render(shape, { isEditing })
|
||||||
|
) : (
|
||||||
|
<RealShape
|
||||||
|
isParent={isParent}
|
||||||
|
id={id}
|
||||||
|
style={style}
|
||||||
|
isEditing={isEditing}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{isParent &&
|
||||||
shape.children.map((shapeId) => (
|
shape.children.map((shapeId) => (
|
||||||
<Shape
|
<Shape
|
||||||
key={shapeId}
|
key={shapeId}
|
||||||
|
@ -68,17 +98,19 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RealShapeProps {
|
interface RealShapeProps {
|
||||||
isGroup: boolean
|
|
||||||
id: string
|
id: string
|
||||||
style: Partial<React.SVGProps<SVGUseElement>>
|
style: Partial<React.SVGProps<SVGUseElement>>
|
||||||
|
isParent: boolean
|
||||||
|
isEditing: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const RealShape = memo(function RealShape({
|
const RealShape = memo(function RealShape({
|
||||||
isGroup,
|
|
||||||
id,
|
id,
|
||||||
style,
|
style,
|
||||||
|
isParent,
|
||||||
|
isEditing,
|
||||||
}: RealShapeProps) {
|
}: RealShapeProps) {
|
||||||
return <StyledShape as="use" data-shy={isGroup} href={'#' + id} {...style} />
|
return <StyledShape as="use" data-shy={isParent} href={'#' + id} {...style} />
|
||||||
})
|
})
|
||||||
|
|
||||||
const StyledShape = styled('path', {
|
const StyledShape = styled('path', {
|
||||||
|
@ -91,11 +123,15 @@ const HoverIndicator = styled('path', {
|
||||||
stroke: '$selected',
|
stroke: '$selected',
|
||||||
strokeLinecap: 'round',
|
strokeLinecap: 'round',
|
||||||
strokeLinejoin: 'round',
|
strokeLinejoin: 'round',
|
||||||
transform: 'all .2s',
|
|
||||||
fill: 'transparent',
|
fill: 'transparent',
|
||||||
filter: 'url(#expand)',
|
filter: 'url(#expand)',
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
ghost: {
|
||||||
|
pointerEvents: 'all',
|
||||||
|
filter: 'none',
|
||||||
|
opacity: 0,
|
||||||
|
},
|
||||||
hollow: {
|
hollow: {
|
||||||
pointerEvents: 'stroke',
|
pointerEvents: 'stroke',
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,19 +1,25 @@
|
||||||
import { useCallback, useRef } from 'react'
|
import { useCallback } from 'react'
|
||||||
import inputs from 'state/inputs'
|
import inputs from 'state/inputs'
|
||||||
import { Edge, Corner } from 'types'
|
import { Edge, Corner } from 'types'
|
||||||
|
|
||||||
import state from '../state'
|
import state from '../state'
|
||||||
|
|
||||||
export default function useBoundsHandleEvents(
|
export default function useBoundsEvents(handle: Edge | Corner | 'rotate') {
|
||||||
handle: Edge | Corner | 'rotate'
|
|
||||||
) {
|
|
||||||
const onPointerDown = useCallback(
|
const onPointerDown = useCallback(
|
||||||
(e) => {
|
(e) => {
|
||||||
if (e.buttons !== 1) return
|
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.currentTarget.setPointerCapture(e.pointerId)
|
e.currentTarget.setPointerCapture(e.pointerId)
|
||||||
state.send('POINTED_BOUNDS_HANDLE', inputs.pointerDown(e, handle))
|
|
||||||
|
if (e.button === 0) {
|
||||||
|
const info = inputs.pointerDown(e, handle)
|
||||||
|
|
||||||
|
if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) {
|
||||||
|
state.send('DOUBLE_POINTED_BOUNDS_HANDLE', info)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.send('POINTED_BOUNDS_HANDLE', info)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[handle]
|
[handle]
|
||||||
)
|
)
|
|
@ -6,7 +6,20 @@ import { getKeyboardEventInfo, metaKey } from 'utils/utils'
|
||||||
export default function useKeyboardEvents() {
|
export default function useKeyboardEvents() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleKeyDown(e: KeyboardEvent) {
|
function handleKeyDown(e: KeyboardEvent) {
|
||||||
if (metaKey(e) && !['i', 'r', 'j'].includes(e.key)) {
|
if (
|
||||||
|
metaKey(e) &&
|
||||||
|
![
|
||||||
|
'a',
|
||||||
|
'i',
|
||||||
|
'r',
|
||||||
|
'j',
|
||||||
|
'ArrowLeft',
|
||||||
|
'ArrowRight',
|
||||||
|
'ArrowUp',
|
||||||
|
'ArrowDown',
|
||||||
|
'z',
|
||||||
|
].includes(e.key)
|
||||||
|
) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ import inputs from 'state/inputs'
|
||||||
|
|
||||||
export default function useShapeEvents(
|
export default function useShapeEvents(
|
||||||
id: string,
|
id: string,
|
||||||
isGroup: boolean,
|
isParent: boolean,
|
||||||
rGroup: MutableRefObject<SVGElement>
|
rGroup: MutableRefObject<SVGElement>
|
||||||
) {
|
) {
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (isGroup) return
|
if (isParent) return
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
rGroup.current.setPointerCapture(e.pointerId)
|
rGroup.current.setPointerCapture(e.pointerId)
|
||||||
|
@ -42,7 +42,7 @@ export default function useShapeEvents(
|
||||||
const handlePointerEnter = useCallback(
|
const handlePointerEnter = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
if (isGroup) {
|
if (isParent) {
|
||||||
state.send('HOVERED_GROUP', inputs.pointerEnter(e, id))
|
state.send('HOVERED_GROUP', inputs.pointerEnter(e, id))
|
||||||
} else {
|
} else {
|
||||||
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
||||||
|
@ -55,7 +55,7 @@ export default function useShapeEvents(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
|
|
||||||
if (isGroup) {
|
if (isParent) {
|
||||||
state.send('MOVED_OVER_GROUP', inputs.pointerEnter(e, id))
|
state.send('MOVED_OVER_GROUP', inputs.pointerEnter(e, id))
|
||||||
} else {
|
} else {
|
||||||
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
||||||
|
@ -67,7 +67,7 @@ export default function useShapeEvents(
|
||||||
const handlePointerLeave = useCallback(
|
const handlePointerLeave = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
if (isGroup) {
|
if (isParent) {
|
||||||
state.send('UNHOVERED_GROUP', { target: id })
|
state.send('UNHOVERED_GROUP', { target: id })
|
||||||
} else {
|
} else {
|
||||||
state.send('UNHOVERED_SHAPE', { target: id })
|
state.send('UNHOVERED_SHAPE', { target: id })
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default class Ray extends CodeShape<RayShape> {
|
||||||
type: ShapeType.Ray,
|
type: ShapeType.Ray,
|
||||||
isGenerated: true,
|
isGenerated: true,
|
||||||
name: 'Ray',
|
name: 'Ray',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
direction: [0, 1],
|
direction: [0, 1],
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { SVGProps } from 'react'
|
import { SVGProps } from 'react'
|
||||||
import { ColorStyle, DashStyle, Shape, ShapeStyles, SizeStyle } from 'types'
|
import { ColorStyle, DashStyle, FontSize, ShapeStyles, SizeStyle } from 'types'
|
||||||
|
|
||||||
export const strokes: Record<ColorStyle, string> = {
|
export const strokes: Record<ColorStyle, string> = {
|
||||||
[ColorStyle.White]: 'rgba(248, 249, 250, 1.000)',
|
[ColorStyle.White]: 'rgba(248, 249, 250, 1.000)',
|
||||||
|
@ -43,6 +43,14 @@ const dashArrays = {
|
||||||
[DashStyle.Dotted]: (sw: number) => `0 ${sw * 1.5}`,
|
[DashStyle.Dotted]: (sw: number) => `0 ${sw * 1.5}`,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fontSizes = {
|
||||||
|
[FontSize.Small]: 16,
|
||||||
|
[FontSize.Medium]: 28,
|
||||||
|
[FontSize.Large]: 32,
|
||||||
|
[FontSize.ExtraLarge]: 72,
|
||||||
|
auto: 'auto',
|
||||||
|
}
|
||||||
|
|
||||||
function getStrokeWidth(size: SizeStyle) {
|
function getStrokeWidth(size: SizeStyle) {
|
||||||
return strokeWidths[size]
|
return strokeWidths[size]
|
||||||
}
|
}
|
||||||
|
@ -51,6 +59,16 @@ function getStrokeDashArray(dash: DashStyle, strokeWidth: number) {
|
||||||
return dashArrays[dash](strokeWidth)
|
return dashArrays[dash](strokeWidth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFontSize(size: FontSize) {
|
||||||
|
return fontSizes[size]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFontStyle(size: FontSize, style: ShapeStyles) {
|
||||||
|
const fontSize = getFontSize(size)
|
||||||
|
|
||||||
|
return `${fontSize}px Verveine Regular`
|
||||||
|
}
|
||||||
|
|
||||||
export function getShapeStyle(
|
export function getShapeStyle(
|
||||||
style: ShapeStyles
|
style: ShapeStyles
|
||||||
): Partial<SVGProps<SVGUseElement>> {
|
): Partial<SVGProps<SVGUseElement>> {
|
||||||
|
|
|
@ -70,7 +70,7 @@ const arrow = registerShapeUtils<ArrowShape>({
|
||||||
type: ShapeType.Arrow,
|
type: ShapeType.Arrow,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Arrow',
|
name: 'Arrow',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point,
|
point,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
|
|
|
@ -18,7 +18,7 @@ const circle = registerShapeUtils<CircleShape>({
|
||||||
type: ShapeType.Circle,
|
type: ShapeType.Circle,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Circle',
|
name: 'Circle',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
|
|
|
@ -17,7 +17,7 @@ const dot = registerShapeUtils<DotShape>({
|
||||||
type: ShapeType.Dot,
|
type: ShapeType.Dot,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Dot',
|
name: 'Dot',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
|
|
|
@ -28,7 +28,7 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
type: ShapeType.Draw,
|
type: ShapeType.Draw,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Draw',
|
name: 'Draw',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
points: [],
|
points: [],
|
||||||
|
|
|
@ -21,7 +21,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
type: ShapeType.Ellipse,
|
type: ShapeType.Ellipse,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Ellipse',
|
name: 'Ellipse',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
radiusX: 1,
|
radiusX: 1,
|
||||||
|
|
|
@ -22,6 +22,8 @@ import { boundsContainPolygon } from 'utils/bounds'
|
||||||
|
|
||||||
const group = registerShapeUtils<GroupShape>({
|
const group = registerShapeUtils<GroupShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
isShy: true,
|
||||||
|
isParent: true,
|
||||||
|
|
||||||
create(props) {
|
create(props) {
|
||||||
return {
|
return {
|
||||||
|
@ -30,7 +32,7 @@ const group = registerShapeUtils<GroupShape>({
|
||||||
type: ShapeType.Group,
|
type: ShapeType.Group,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Group',
|
name: 'Group',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
size: [1, 1],
|
size: [1, 1],
|
||||||
|
|
|
@ -8,7 +8,6 @@ import {
|
||||||
ShapeBinding,
|
ShapeBinding,
|
||||||
Mutable,
|
Mutable,
|
||||||
ShapeByType,
|
ShapeByType,
|
||||||
Data,
|
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import * as vec from 'utils/vec'
|
import * as vec from 'utils/vec'
|
||||||
import {
|
import {
|
||||||
|
@ -32,6 +31,7 @@ import ray from './ray'
|
||||||
import draw from './draw'
|
import draw from './draw'
|
||||||
import arrow from './arrow'
|
import arrow from './arrow'
|
||||||
import group from './group'
|
import group from './group'
|
||||||
|
import text from './text'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Shape Utiliies
|
Shape Utiliies
|
||||||
|
@ -51,12 +51,24 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
// Whether to show transform controls when this shape is selected.
|
// Whether to show transform controls when this shape is selected.
|
||||||
canTransform: boolean
|
canTransform: boolean
|
||||||
|
|
||||||
// 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
|
// Whether the shape's style can be filled.
|
||||||
canStyleFill: boolean
|
canStyleFill: boolean
|
||||||
|
|
||||||
|
// Whether the shape may be edited in an editing mode
|
||||||
|
canEdit: boolean
|
||||||
|
|
||||||
|
// Whether the shape is a foreign object.
|
||||||
|
isForeignObject: boolean
|
||||||
|
|
||||||
|
// Whether the shape can contain other shapes.
|
||||||
|
isParent: boolean
|
||||||
|
|
||||||
|
// Whether the shape is only shown when on hovered.
|
||||||
|
isShy: boolean
|
||||||
|
|
||||||
// Create a new shape.
|
// Create a new shape.
|
||||||
create(props: Partial<K>): K
|
create(props: Partial<K>): K
|
||||||
|
|
||||||
|
@ -148,11 +160,21 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
handle: Partial<K['handles']>
|
handle: Partial<K['handles']>
|
||||||
): ShapeUtility<K>
|
): ShapeUtility<K>
|
||||||
|
|
||||||
|
// Respond when a user double clicks the shape's bounds.
|
||||||
|
onBoundsReset(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
|
||||||
|
|
||||||
|
// Respond when a user double clicks the center of the shape.
|
||||||
|
onDoubleFocus(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
|
||||||
|
|
||||||
// Clean up changes when a session ends.
|
// Clean up changes when a session ends.
|
||||||
onSessionComplete(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
|
onSessionComplete(this: ShapeUtility<K>, shape: Mutable<K>): ShapeUtility<K>
|
||||||
|
|
||||||
// Render a shape to JSX.
|
// Render a shape to JSX.
|
||||||
render(this: ShapeUtility<K>, shape: K): JSX.Element
|
render(
|
||||||
|
this: ShapeUtility<K>,
|
||||||
|
shape: K,
|
||||||
|
info: { isEditing: boolean }
|
||||||
|
): JSX.Element
|
||||||
|
|
||||||
// Get the bounds of the a shape.
|
// Get the bounds of the a shape.
|
||||||
getBounds(this: ShapeUtility<K>, shape: K): Bounds
|
getBounds(this: ShapeUtility<K>, shape: K): Bounds
|
||||||
|
@ -168,6 +190,8 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
|
|
||||||
// Test whether bounds collide with or contain a shape.
|
// Test whether bounds collide with or contain a shape.
|
||||||
hitTestBounds(this: ShapeUtility<K>, shape: K, bounds: Bounds): boolean
|
hitTestBounds(this: ShapeUtility<K>, shape: K, bounds: Bounds): boolean
|
||||||
|
|
||||||
|
getShouldDelete(this: ShapeUtility<K>, shape: K): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// A mapping of shape types to shape utilities.
|
// A mapping of shape types to shape utilities.
|
||||||
|
@ -181,7 +205,7 @@ const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
|
||||||
[ShapeType.Ray]: ray,
|
[ShapeType.Ray]: ray,
|
||||||
[ShapeType.Draw]: draw,
|
[ShapeType.Draw]: draw,
|
||||||
[ShapeType.Arrow]: arrow,
|
[ShapeType.Arrow]: arrow,
|
||||||
[ShapeType.Text]: arrow,
|
[ShapeType.Text]: text,
|
||||||
[ShapeType.Group]: group,
|
[ShapeType.Group]: group,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -191,7 +215,7 @@ const shapeUtilityMap: Record<ShapeType, ShapeUtility<Shape>> = {
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function getShapeUtils<T extends Shape>(shape: T): ShapeUtility<T> {
|
export function getShapeUtils<T extends Shape>(shape: T): ShapeUtility<T> {
|
||||||
return shapeUtilityMap[shape.type] as ShapeUtility<T>
|
return shapeUtilityMap[shape?.type] as ShapeUtility<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
|
@ -200,6 +224,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
canTransform: true,
|
canTransform: true,
|
||||||
canChangeAspectRatio: true,
|
canChangeAspectRatio: true,
|
||||||
canStyleFill: true,
|
canStyleFill: true,
|
||||||
|
canEdit: false,
|
||||||
|
isShy: false,
|
||||||
|
isParent: false,
|
||||||
|
isForeignObject: false,
|
||||||
|
|
||||||
create(props) {
|
create(props) {
|
||||||
return {
|
return {
|
||||||
|
@ -207,7 +235,7 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
name: 'Shape',
|
name: 'Shape',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
isAspectRatioLocked: false,
|
isAspectRatioLocked: false,
|
||||||
|
@ -262,6 +290,14 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onDoubleFocus() {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
onBoundsReset() {
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
onSessionComplete() {
|
onSessionComplete() {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
@ -313,6 +349,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
Object.assign(shape.style, style)
|
Object.assign(shape.style, style)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getShouldDelete(shape) {
|
||||||
|
return false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ const line = registerShapeUtils<LineShape>({
|
||||||
type: ShapeType.Line,
|
type: ShapeType.Line,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Line',
|
name: 'Line',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
direction: [0, 0],
|
direction: [0, 0],
|
||||||
|
|
|
@ -17,7 +17,7 @@ const polyline = registerShapeUtils<PolylineShape>({
|
||||||
type: ShapeType.Polyline,
|
type: ShapeType.Polyline,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Polyline',
|
name: 'Polyline',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
points: [[0, 0]],
|
points: [[0, 0]],
|
||||||
|
|
|
@ -18,7 +18,7 @@ const ray = registerShapeUtils<RayShape>({
|
||||||
type: ShapeType.Ray,
|
type: ShapeType.Ray,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Ray',
|
name: 'Ray',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
direction: [0, 1],
|
direction: [0, 1],
|
||||||
|
|
|
@ -24,7 +24,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
type: ShapeType.Rectangle,
|
type: ShapeType.Rectangle,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Rectangle',
|
name: 'Rectangle',
|
||||||
parentId: 'page0',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
size: [1, 1],
|
size: [1, 1],
|
||||||
|
|
192
lib/shape-utils/text.tsx
Normal file
192
lib/shape-utils/text.tsx
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
import { v4 as uuid } from 'uuid'
|
||||||
|
import * as vec from 'utils/vec'
|
||||||
|
import { TextShape, ShapeType, FontSize } from 'types'
|
||||||
|
import { registerShapeUtils } from './index'
|
||||||
|
import { defaultStyle, getFontStyle, getShapeStyle } from 'lib/shape-styles'
|
||||||
|
import styled from 'styles'
|
||||||
|
import state from 'state'
|
||||||
|
import { useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
// A div used for measurement
|
||||||
|
|
||||||
|
if (document.getElementById('__textMeasure')) {
|
||||||
|
document.getElementById('__textMeasure').remove()
|
||||||
|
}
|
||||||
|
|
||||||
|
const mdiv = document.createElement('pre')
|
||||||
|
mdiv.id = '__textMeasure'
|
||||||
|
mdiv.style.whiteSpace = 'pre'
|
||||||
|
mdiv.style.width = 'auto'
|
||||||
|
mdiv.style.border = '1px solid red'
|
||||||
|
mdiv.style.padding = '4px'
|
||||||
|
mdiv.style.margin = '0px'
|
||||||
|
mdiv.style.opacity = '0'
|
||||||
|
mdiv.style.position = 'absolute'
|
||||||
|
mdiv.style.top = '-500px'
|
||||||
|
mdiv.style.left = '0px'
|
||||||
|
mdiv.style.zIndex = '9999'
|
||||||
|
document.body.appendChild(mdiv)
|
||||||
|
|
||||||
|
const text = registerShapeUtils<TextShape>({
|
||||||
|
isForeignObject: true,
|
||||||
|
canChangeAspectRatio: false,
|
||||||
|
canEdit: true,
|
||||||
|
|
||||||
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
|
create(props) {
|
||||||
|
return {
|
||||||
|
id: uuid(),
|
||||||
|
seed: Math.random(),
|
||||||
|
type: ShapeType.Text,
|
||||||
|
isGenerated: false,
|
||||||
|
name: 'Text',
|
||||||
|
parentId: 'page1',
|
||||||
|
childIndex: 0,
|
||||||
|
point: [0, 0],
|
||||||
|
rotation: 0,
|
||||||
|
isAspectRatioLocked: false,
|
||||||
|
isLocked: false,
|
||||||
|
isHidden: false,
|
||||||
|
style: defaultStyle,
|
||||||
|
text: '',
|
||||||
|
size: 'auto',
|
||||||
|
fontSize: FontSize.Medium,
|
||||||
|
...props,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render(shape, { isEditing }) {
|
||||||
|
const { id, text, style } = shape
|
||||||
|
const styles = getShapeStyle(style)
|
||||||
|
|
||||||
|
const font = getFontStyle(shape.fontSize, shape.style)
|
||||||
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
state.send('EDITED_SHAPE', { change: { text: e.currentTarget.value } })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<foreignObject
|
||||||
|
id={id}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={bounds.width}
|
||||||
|
height={bounds.height}
|
||||||
|
pointerEvents="none"
|
||||||
|
>
|
||||||
|
<StyledText
|
||||||
|
key={id}
|
||||||
|
style={{
|
||||||
|
font,
|
||||||
|
color: styles.fill,
|
||||||
|
}}
|
||||||
|
value={text}
|
||||||
|
onChange={handleChange}
|
||||||
|
isEditing={isEditing}
|
||||||
|
onFocus={(e) => e.currentTarget.select()}
|
||||||
|
/>
|
||||||
|
</foreignObject>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
getBounds(shape) {
|
||||||
|
const [minX, minY] = shape.point
|
||||||
|
let width: number
|
||||||
|
let height: number
|
||||||
|
|
||||||
|
if (shape.size === 'auto') {
|
||||||
|
// Calculate a size by rendering text into a div
|
||||||
|
mdiv.innerHTML = shape.text + ' '
|
||||||
|
mdiv.style.font = getFontStyle(shape.fontSize, shape.style)
|
||||||
|
;[width, height] = [mdiv.offsetWidth, mdiv.offsetHeight]
|
||||||
|
} else {
|
||||||
|
// Use the shape's explicit size for width and height.
|
||||||
|
;[width, height] = shape.size
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minX,
|
||||||
|
maxX: minX + width,
|
||||||
|
minY,
|
||||||
|
maxY: minY + height,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
hitTest(shape, test) {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
|
||||||
|
if (shape.rotation === 0 && !shape.isAspectRatioLocked) {
|
||||||
|
shape.size = [bounds.width, bounds.height]
|
||||||
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
} else {
|
||||||
|
if (initialShape.size === 'auto') return
|
||||||
|
|
||||||
|
shape.size = vec.mul(
|
||||||
|
initialShape.size,
|
||||||
|
Math.min(Math.abs(scaleX), Math.abs(scaleY))
|
||||||
|
)
|
||||||
|
|
||||||
|
shape.point = [
|
||||||
|
bounds.minX +
|
||||||
|
(bounds.width - shape.size[0]) *
|
||||||
|
(scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
|
||||||
|
bounds.minY +
|
||||||
|
(bounds.height - shape.size[1]) *
|
||||||
|
(scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
|
||||||
|
]
|
||||||
|
|
||||||
|
shape.rotation =
|
||||||
|
(scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
|
||||||
|
? -initialShape.rotation
|
||||||
|
: initialShape.rotation
|
||||||
|
}
|
||||||
|
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
transformSingle(shape, bounds) {
|
||||||
|
shape.size = [bounds.width, bounds.height]
|
||||||
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
onBoundsReset(shape) {
|
||||||
|
shape.size = 'auto'
|
||||||
|
return this
|
||||||
|
},
|
||||||
|
|
||||||
|
getShouldDelete(shape) {
|
||||||
|
return shape.text.length === 0
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default text
|
||||||
|
|
||||||
|
const StyledText = styled('textarea', {
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
border: 'none',
|
||||||
|
padding: '4px',
|
||||||
|
whiteSpace: 'pre',
|
||||||
|
resize: 'none',
|
||||||
|
minHeight: 1,
|
||||||
|
minWidth: 1,
|
||||||
|
outline: 'none',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
overflow: 'hidden',
|
||||||
|
|
||||||
|
variants: {
|
||||||
|
isEditing: {
|
||||||
|
true: {
|
||||||
|
backgroundColor: '$boundsBg',
|
||||||
|
pointerEvents: 'all',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
BIN
public/VerveineRegular.woff
Normal file
BIN
public/VerveineRegular.woff
Normal file
Binary file not shown.
|
@ -1,4 +0,0 @@
|
||||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.1 KiB |
40
state/commands/edit.ts
Normal file
40
state/commands/edit.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import Command from './command'
|
||||||
|
import history from '../history'
|
||||||
|
import { Data } from 'types'
|
||||||
|
import { getPage, getShape } from 'utils/utils'
|
||||||
|
import { EditSnapshot } from 'state/sessions/edit-session'
|
||||||
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
|
export default function handleCommand(
|
||||||
|
data: Data,
|
||||||
|
before: EditSnapshot,
|
||||||
|
after: EditSnapshot
|
||||||
|
) {
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name: 'edited_shape',
|
||||||
|
category: 'canvas',
|
||||||
|
do(data, isInitial) {
|
||||||
|
const { initialShape, currentPageId } = after
|
||||||
|
|
||||||
|
const page = getPage(data, currentPageId)
|
||||||
|
|
||||||
|
page.shapes[initialShape.id] = initialShape
|
||||||
|
|
||||||
|
const shape = page.shapes[initialShape.id]
|
||||||
|
|
||||||
|
if (getShapeUtils(shape).getShouldDelete(shape)) {
|
||||||
|
delete page.shapes[initialShape.id]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
const { initialShape, currentPageId } = before
|
||||||
|
|
||||||
|
const page = getPage(data, currentPageId)
|
||||||
|
|
||||||
|
page.shapes[initialShape.id] = initialShape
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -23,6 +23,8 @@ import transform from './transform'
|
||||||
import transformSingle from './transform-single'
|
import transformSingle from './transform-single'
|
||||||
import translate from './translate'
|
import translate from './translate'
|
||||||
import ungroup from './ungroup'
|
import ungroup from './ungroup'
|
||||||
|
import edit from './edit'
|
||||||
|
import resetBounds from './reset-bounds'
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
align,
|
align,
|
||||||
|
@ -35,12 +37,14 @@ const commands = {
|
||||||
distribute,
|
distribute,
|
||||||
draw,
|
draw,
|
||||||
duplicate,
|
duplicate,
|
||||||
|
edit,
|
||||||
generate,
|
generate,
|
||||||
group,
|
group,
|
||||||
handle,
|
handle,
|
||||||
move,
|
move,
|
||||||
moveToPage,
|
moveToPage,
|
||||||
nudge,
|
nudge,
|
||||||
|
resetBounds,
|
||||||
rotate,
|
rotate,
|
||||||
rotateCcw,
|
rotateCcw,
|
||||||
stretch,
|
stretch,
|
||||||
|
|
31
state/commands/reset-bounds.ts
Normal file
31
state/commands/reset-bounds.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import Command from './command'
|
||||||
|
import history from '../history'
|
||||||
|
import { Data } from 'types'
|
||||||
|
import { getPage, getSelectedShapes } from 'utils/utils'
|
||||||
|
import { current } from 'immer'
|
||||||
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
|
export default function resetBoundsCommand(data: Data) {
|
||||||
|
const initialShapes = Object.fromEntries(
|
||||||
|
getSelectedShapes(current(data)).map((shape) => [shape.id, shape])
|
||||||
|
)
|
||||||
|
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name: 'reset_bounds',
|
||||||
|
category: 'canvas',
|
||||||
|
do(data) {
|
||||||
|
getSelectedShapes(data).forEach((shape) => {
|
||||||
|
getShapeUtils(shape).onBoundsReset(shape)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
const page = getPage(data)
|
||||||
|
getSelectedShapes(data).forEach((shape) => {
|
||||||
|
page.shapes[shape.id] = initialShapes[shape.id]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import { Data, ShapeType } from 'types'
|
import { Data, FontSize, ShapeType } from 'types'
|
||||||
import shapeUtils from 'lib/shape-utils'
|
import shapeUtils from 'lib/shape-utils'
|
||||||
|
|
||||||
export const defaultDocument: Data['document'] = {
|
export const defaultDocument: Data['document'] = {
|
||||||
id: '0001',
|
id: '0001',
|
||||||
name: 'My Document',
|
name: 'My Default Document',
|
||||||
pages: {
|
pages: {
|
||||||
page1: {
|
page1: {
|
||||||
id: 'page1',
|
id: 'page1',
|
||||||
|
@ -11,6 +11,40 @@ export const defaultDocument: Data['document'] = {
|
||||||
name: 'Page 1',
|
name: 'Page 1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
shapes: {
|
shapes: {
|
||||||
|
// textShape0: shapeUtils[ShapeType.Text].create({
|
||||||
|
// id: 'textShape0',
|
||||||
|
// point: [0, 0],
|
||||||
|
// text: 'Short',
|
||||||
|
// childIndex: 0,
|
||||||
|
// }),
|
||||||
|
// textShape1: shapeUtils[ShapeType.Text].create({
|
||||||
|
// id: 'textShape1',
|
||||||
|
// point: [100, 150],
|
||||||
|
// fontSize: FontSize.Small,
|
||||||
|
// text: 'Well, this is a pretty long title. I hope it all still works',
|
||||||
|
// childIndex: 1,
|
||||||
|
// }),
|
||||||
|
// textShape2: shapeUtils[ShapeType.Text].create({
|
||||||
|
// id: 'textShape2',
|
||||||
|
// point: [100, 200],
|
||||||
|
// fontSize: FontSize.Medium,
|
||||||
|
// text: 'Well, this is a pretty long title. I hope it all still works',
|
||||||
|
// childIndex: 2,
|
||||||
|
// }),
|
||||||
|
// textShape3: shapeUtils[ShapeType.Text].create({
|
||||||
|
// id: 'textShape3',
|
||||||
|
// point: [100, 250],
|
||||||
|
// fontSize: FontSize.Large,
|
||||||
|
// text: 'Well, this is a pretty long title. I hope it all still works',
|
||||||
|
// childIndex: 3,
|
||||||
|
// }),
|
||||||
|
// textShape4: shapeUtils[ShapeType.Text].create({
|
||||||
|
// id: 'textShape4',
|
||||||
|
// point: [100, 300],
|
||||||
|
// fontSize: FontSize.ExtraLarge,
|
||||||
|
// text: 'Well, this is a pretty long title. I hope it all still works',
|
||||||
|
// childIndex: 4,
|
||||||
|
// }),
|
||||||
// arrowShape0: shapeUtils[ShapeType.Arrow].create({
|
// arrowShape0: shapeUtils[ShapeType.Arrow].create({
|
||||||
// id: 'arrowShape0',
|
// id: 'arrowShape0',
|
||||||
// point: [200, 200],
|
// point: [200, 200],
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { PointerInfo } from 'types'
|
import { PointerInfo } from 'types'
|
||||||
|
import * as vec from 'utils/vec'
|
||||||
import { isDarwin, getPoint } from 'utils/utils'
|
import { isDarwin, getPoint } from 'utils/utils'
|
||||||
|
|
||||||
const DOUBLE_CLICK_DURATION = 300
|
const DOUBLE_CLICK_DURATION = 300
|
||||||
|
|
||||||
class Inputs {
|
class Inputs {
|
||||||
activePointerId?: number
|
activePointerId?: number
|
||||||
lastPointerDownTime = 0
|
lastPointerUpTime = 0
|
||||||
points: Record<string, PointerInfo> = {}
|
points: Record<string, PointerInfo> = {}
|
||||||
|
|
||||||
touchStart(e: TouchEvent | React.TouchEvent, target: string) {
|
touchStart(e: TouchEvent | React.TouchEvent, target: string) {
|
||||||
|
@ -119,7 +120,7 @@ class Inputs {
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
pointerUp(e: PointerEvent | React.PointerEvent) {
|
pointerUp = (e: PointerEvent | React.PointerEvent) => {
|
||||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
|
|
||||||
const prev = this.points[e.pointerId]
|
const prev = this.points[e.pointerId]
|
||||||
|
@ -137,24 +138,31 @@ class Inputs {
|
||||||
|
|
||||||
delete this.points[e.pointerId]
|
delete this.points[e.pointerId]
|
||||||
delete this.activePointerId
|
delete this.activePointerId
|
||||||
this.lastPointerDownTime = Date.now()
|
|
||||||
|
if (vec.dist(info.origin, info.point) < 8) {
|
||||||
|
this.lastPointerUpTime = Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
wheel(e: WheelEvent) {
|
wheel = (e: WheelEvent) => {
|
||||||
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
const { shiftKey, ctrlKey, metaKey, altKey } = e
|
||||||
return { point: getPoint(e), shiftKey, ctrlKey, metaKey, altKey }
|
return { point: getPoint(e), shiftKey, ctrlKey, metaKey, altKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
canAccept(pointerId: PointerEvent['pointerId']) {
|
canAccept = (pointerId: PointerEvent['pointerId']) => {
|
||||||
return (
|
return (
|
||||||
this.activePointerId === undefined || this.activePointerId === pointerId
|
this.activePointerId === undefined || this.activePointerId === pointerId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
isDoubleClick() {
|
isDoubleClick() {
|
||||||
return Date.now() - this.lastPointerDownTime < DOUBLE_CLICK_DURATION
|
const { origin, point } = this.pointer
|
||||||
|
return (
|
||||||
|
Date.now() - this.lastPointerUpTime < DOUBLE_CLICK_DURATION &&
|
||||||
|
vec.dist(origin, point) < 8
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
get pointer() {
|
get pointer() {
|
||||||
|
|
51
state/sessions/edit-session.ts
Normal file
51
state/sessions/edit-session.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import { Data, LineShape, RayShape, Shape } from 'types'
|
||||||
|
import * as vec from 'utils/vec'
|
||||||
|
import BaseSession from './base-session'
|
||||||
|
import commands from 'state/commands'
|
||||||
|
import { current } from 'immer'
|
||||||
|
import {
|
||||||
|
getPage,
|
||||||
|
getSelectedIds,
|
||||||
|
getSelectedShapes,
|
||||||
|
getShape,
|
||||||
|
} from 'utils/utils'
|
||||||
|
import { getShapeUtils } from 'lib/shape-utils'
|
||||||
|
|
||||||
|
export default class EditSession extends BaseSession {
|
||||||
|
snapshot: EditSnapshot
|
||||||
|
|
||||||
|
constructor(data: Data) {
|
||||||
|
super(data)
|
||||||
|
this.snapshot = getEditSnapshot(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
update(data: Data, change: Partial<Shape>) {
|
||||||
|
const initialShape = this.snapshot.initialShape
|
||||||
|
const shape = getShape(data, initialShape.id)
|
||||||
|
const utils = getShapeUtils(shape)
|
||||||
|
Object.entries(change).forEach(([key, value]) => {
|
||||||
|
utils.setProperty(shape, key as keyof Shape, value as Shape[keyof Shape])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel(data: Data) {
|
||||||
|
const initialShape = this.snapshot.initialShape
|
||||||
|
const page = getPage(data)
|
||||||
|
page.shapes[initialShape.id] = initialShape
|
||||||
|
}
|
||||||
|
|
||||||
|
complete(data: Data) {
|
||||||
|
commands.edit(data, this.snapshot, getEditSnapshot(data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEditSnapshot(data: Data) {
|
||||||
|
const initialShape = getSelectedShapes(current(data))[0]
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentPageId: data.currentPageId,
|
||||||
|
initialShape,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EditSnapshot = ReturnType<typeof getEditSnapshot>
|
|
@ -8,6 +8,7 @@ import TransformSession from './transform-session'
|
||||||
import TransformSingleSession from './transform-single-session'
|
import TransformSingleSession from './transform-single-session'
|
||||||
import TranslateSession from './translate-session'
|
import TranslateSession from './translate-session'
|
||||||
import HandleSession from './handle-session'
|
import HandleSession from './handle-session'
|
||||||
|
import EditSession from './edit-session'
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ArrowSession,
|
ArrowSession,
|
||||||
|
@ -20,4 +21,5 @@ export {
|
||||||
TransformSingleSession,
|
TransformSingleSession,
|
||||||
TranslateSession,
|
TranslateSession,
|
||||||
HandleSession,
|
HandleSession,
|
||||||
|
EditSession,
|
||||||
}
|
}
|
||||||
|
|
166
state/state.ts
166
state/state.ts
|
@ -25,6 +25,7 @@ import {
|
||||||
getCameraZoom,
|
getCameraZoom,
|
||||||
getSelectedIds,
|
getSelectedIds,
|
||||||
setSelectedIds,
|
setSelectedIds,
|
||||||
|
getPageState,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
|
@ -42,6 +43,7 @@ import {
|
||||||
DashStyle,
|
DashStyle,
|
||||||
SizeStyle,
|
SizeStyle,
|
||||||
ColorStyle,
|
ColorStyle,
|
||||||
|
FontSize,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import session from './session'
|
import session from './session'
|
||||||
import { pointInBounds } from 'utils/bounds'
|
import { pointInBounds } from 'utils/bounds'
|
||||||
|
@ -62,6 +64,7 @@ const initialData: Data = {
|
||||||
size: SizeStyle.Medium,
|
size: SizeStyle.Medium,
|
||||||
color: ColorStyle.Black,
|
color: ColorStyle.Black,
|
||||||
dash: DashStyle.Solid,
|
dash: DashStyle.Solid,
|
||||||
|
fontSize: FontSize.Medium,
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
},
|
},
|
||||||
activeTool: 'select',
|
activeTool: 'select',
|
||||||
|
@ -69,6 +72,7 @@ const initialData: Data = {
|
||||||
boundsRotation: 0,
|
boundsRotation: 0,
|
||||||
pointedId: null,
|
pointedId: null,
|
||||||
hoveredId: null,
|
hoveredId: null,
|
||||||
|
editingId: null,
|
||||||
currentPageId: 'page1',
|
currentPageId: 'page1',
|
||||||
currentParentId: 'page1',
|
currentParentId: 'page1',
|
||||||
currentCodeFileId: 'file0',
|
currentCodeFileId: 'file0',
|
||||||
|
@ -117,47 +121,16 @@ const state = createState({
|
||||||
else: ['zoomCameraToFit', 'zoomCameraToActual'],
|
else: ['zoomCameraToFit', 'zoomCameraToActual'],
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
ZOOMED_CAMERA: {
|
|
||||||
do: 'zoomCamera',
|
|
||||||
},
|
|
||||||
PANNED_CAMERA: {
|
|
||||||
do: 'panCamera',
|
|
||||||
},
|
|
||||||
ZOOMED_TO_ACTUAL: {
|
|
||||||
if: 'hasSelection',
|
|
||||||
do: 'zoomCameraToSelectionActual',
|
|
||||||
else: 'zoomCameraToActual',
|
|
||||||
},
|
|
||||||
ZOOMED_TO_SELECTION: {
|
|
||||||
if: 'hasSelection',
|
|
||||||
do: 'zoomCameraToSelection',
|
|
||||||
},
|
|
||||||
ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
|
|
||||||
ZOOMED_IN: 'zoomIn',
|
|
||||||
ZOOMED_OUT: 'zoomOut',
|
|
||||||
RESET_CAMERA: 'resetCamera',
|
|
||||||
TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
|
TOGGLED_SHAPE_LOCK: { if: 'hasSelection', do: 'lockSelection' },
|
||||||
TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
|
TOGGLED_SHAPE_HIDE: { if: 'hasSelection', do: 'hideSelection' },
|
||||||
TOGGLED_SHAPE_ASPECT_LOCK: {
|
TOGGLED_SHAPE_ASPECT_LOCK: {
|
||||||
if: 'hasSelection',
|
if: 'hasSelection',
|
||||||
do: 'aspectLockSelection',
|
do: 'aspectLockSelection',
|
||||||
},
|
},
|
||||||
SELECTED_SELECT_TOOL: { to: 'selecting' },
|
|
||||||
SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
|
|
||||||
SELECTED_ARROW_TOOL: { unless: 'isReadOnly', to: 'arrow' },
|
|
||||||
SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
|
|
||||||
SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
|
|
||||||
SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
|
|
||||||
SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
|
|
||||||
SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
|
|
||||||
SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
|
|
||||||
SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
|
|
||||||
TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
|
TOGGLED_CODE_PANEL_OPEN: 'toggleCodePanel',
|
||||||
TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
|
TOGGLED_STYLE_PANEL_OPEN: 'toggleStylePanel',
|
||||||
POINTED_CANVAS: ['closeStylePanel', 'clearCurrentParentId'],
|
POINTED_CANVAS: ['closeStylePanel', 'clearCurrentParentId'],
|
||||||
CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
|
CHANGED_STYLE: ['updateStyles', 'applyStylesToSelection'],
|
||||||
SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
|
|
||||||
NUDGED: { do: 'nudgeSelection' },
|
|
||||||
USED_PEN_DEVICE: 'enablePenLock',
|
USED_PEN_DEVICE: 'enablePenLock',
|
||||||
DISABLED_PEN_LOCK: 'disablePenLock',
|
DISABLED_PEN_LOCK: 'disablePenLock',
|
||||||
CLEARED_PAGE: {
|
CLEARED_PAGE: {
|
||||||
|
@ -169,6 +142,9 @@ const state = createState({
|
||||||
CREATED_PAGE: ['clearSelectedIds', 'createPage'],
|
CREATED_PAGE: ['clearSelectedIds', 'createPage'],
|
||||||
DELETED_PAGE: { unless: 'hasOnlyOnePage', do: 'deletePage' },
|
DELETED_PAGE: { unless: 'hasOnlyOnePage', do: 'deletePage' },
|
||||||
LOADED_FROM_FILE: 'loadDocumentFromJson',
|
LOADED_FROM_FILE: 'loadDocumentFromJson',
|
||||||
|
PANNED_CAMERA: {
|
||||||
|
do: 'panCamera',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
initial: 'selecting',
|
initial: 'selecting',
|
||||||
states: {
|
states: {
|
||||||
|
@ -206,6 +182,34 @@ const state = createState({
|
||||||
if: ['hasSelection', 'selectionIncludesGroups'],
|
if: ['hasSelection', 'selectionIncludesGroups'],
|
||||||
do: 'ungroupSelection',
|
do: 'ungroupSelection',
|
||||||
},
|
},
|
||||||
|
SELECTED_ALL: { to: 'selecting', do: 'selectAll' },
|
||||||
|
NUDGED: { do: 'nudgeSelection' },
|
||||||
|
SELECTED_SELECT_TOOL: { to: 'selecting' },
|
||||||
|
SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
|
||||||
|
SELECTED_ARROW_TOOL: { unless: 'isReadOnly', to: 'arrow' },
|
||||||
|
SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
|
||||||
|
SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
|
||||||
|
SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
|
||||||
|
SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
|
||||||
|
SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
|
||||||
|
SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
|
||||||
|
SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
|
||||||
|
ZOOMED_CAMERA: {
|
||||||
|
do: 'zoomCamera',
|
||||||
|
},
|
||||||
|
ZOOMED_TO_ACTUAL: {
|
||||||
|
if: 'hasSelection',
|
||||||
|
do: 'zoomCameraToSelectionActual',
|
||||||
|
else: 'zoomCameraToActual',
|
||||||
|
},
|
||||||
|
ZOOMED_TO_SELECTION: {
|
||||||
|
if: 'hasSelection',
|
||||||
|
do: 'zoomCameraToSelection',
|
||||||
|
},
|
||||||
|
ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
|
||||||
|
ZOOMED_IN: 'zoomIn',
|
||||||
|
ZOOMED_OUT: 'zoomOut',
|
||||||
|
RESET_CAMERA: 'resetCamera',
|
||||||
},
|
},
|
||||||
initial: 'notPointing',
|
initial: 'notPointing',
|
||||||
states: {
|
states: {
|
||||||
|
@ -226,6 +230,16 @@ const state = createState({
|
||||||
to: 'rotatingSelection',
|
to: 'rotatingSelection',
|
||||||
else: { to: 'transformingSelection' },
|
else: { to: 'transformingSelection' },
|
||||||
},
|
},
|
||||||
|
STARTED_EDITING_SHAPE: {
|
||||||
|
get: 'firstSelectedShape',
|
||||||
|
if: ['hasSingleSelection', 'canEditSelectedShape'],
|
||||||
|
do: 'setEditingId',
|
||||||
|
to: 'editingShape',
|
||||||
|
},
|
||||||
|
DOUBLE_POINTED_BOUNDS_HANDLE: {
|
||||||
|
if: 'hasSingleSelection',
|
||||||
|
do: 'resetShapeBounds',
|
||||||
|
},
|
||||||
POINTED_HANDLE: { to: 'translatingHandles' },
|
POINTED_HANDLE: { to: 'translatingHandles' },
|
||||||
MOVED_OVER_SHAPE: {
|
MOVED_OVER_SHAPE: {
|
||||||
if: 'pointHitsShape',
|
if: 'pointHitsShape',
|
||||||
|
@ -240,6 +254,16 @@ const state = createState({
|
||||||
},
|
},
|
||||||
UNHOVERED_SHAPE: 'clearHoveredId',
|
UNHOVERED_SHAPE: 'clearHoveredId',
|
||||||
DOUBLE_POINTED_SHAPE: [
|
DOUBLE_POINTED_SHAPE: [
|
||||||
|
'setPointedId',
|
||||||
|
{
|
||||||
|
if: 'isPointedShapeSelected',
|
||||||
|
then: {
|
||||||
|
get: 'firstSelectedShape',
|
||||||
|
if: 'canEditSelectedShape',
|
||||||
|
do: 'setEditingId',
|
||||||
|
to: 'editingShape',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
unless: 'isPressingShiftKey',
|
unless: 'isPressingShiftKey',
|
||||||
do: [
|
do: [
|
||||||
|
@ -385,6 +409,15 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
editingShape: {
|
||||||
|
onEnter: 'startEditSession',
|
||||||
|
onExit: 'clearEditingId',
|
||||||
|
on: {
|
||||||
|
EDITED_SHAPE: { do: 'updateEditSession' },
|
||||||
|
BLURRED_SHAPE: { do: 'completeSession', to: 'selecting' },
|
||||||
|
CANCELLED: { do: 'cancelSession', to: 'selecting' },
|
||||||
|
},
|
||||||
|
},
|
||||||
pinching: {
|
pinching: {
|
||||||
on: {
|
on: {
|
||||||
// Pinching uses hacks.fastPinchCamera
|
// Pinching uses hacks.fastPinchCamera
|
||||||
|
@ -414,6 +447,35 @@ const state = createState({
|
||||||
to: 'pinching.toolPinching',
|
to: 'pinching.toolPinching',
|
||||||
},
|
},
|
||||||
TOGGLED_TOOL_LOCK: 'toggleToolLock',
|
TOGGLED_TOOL_LOCK: 'toggleToolLock',
|
||||||
|
SELECTED_SELECT_TOOL: { to: 'selecting' },
|
||||||
|
SELECTED_DRAW_TOOL: { unless: 'isReadOnly', to: 'draw' },
|
||||||
|
SELECTED_ARROW_TOOL: { unless: 'isReadOnly', to: 'arrow' },
|
||||||
|
SELECTED_DOT_TOOL: { unless: 'isReadOnly', to: 'dot' },
|
||||||
|
SELECTED_CIRCLE_TOOL: { unless: 'isReadOnly', to: 'circle' },
|
||||||
|
SELECTED_ELLIPSE_TOOL: { unless: 'isReadOnly', to: 'ellipse' },
|
||||||
|
SELECTED_RAY_TOOL: { unless: 'isReadOnly', to: 'ray' },
|
||||||
|
SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
|
||||||
|
SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
|
||||||
|
SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
|
||||||
|
ZOOMED_CAMERA: {
|
||||||
|
do: 'zoomCamera',
|
||||||
|
},
|
||||||
|
PANNED_CAMERA: {
|
||||||
|
do: 'panCamera',
|
||||||
|
},
|
||||||
|
ZOOMED_TO_ACTUAL: {
|
||||||
|
if: 'hasSelection',
|
||||||
|
do: 'zoomCameraToSelectionActual',
|
||||||
|
else: 'zoomCameraToActual',
|
||||||
|
},
|
||||||
|
ZOOMED_TO_SELECTION: {
|
||||||
|
if: 'hasSelection',
|
||||||
|
do: 'zoomCameraToSelection',
|
||||||
|
},
|
||||||
|
ZOOMED_TO_FIT: ['zoomCameraToFit', 'zoomCameraToActual'],
|
||||||
|
ZOOMED_IN: 'zoomIn',
|
||||||
|
ZOOMED_OUT: 'zoomOut',
|
||||||
|
RESET_CAMERA: 'resetCamera',
|
||||||
},
|
},
|
||||||
states: {
|
states: {
|
||||||
draw: {
|
draw: {
|
||||||
|
@ -781,6 +843,9 @@ const state = createState({
|
||||||
newRectangle() {
|
newRectangle() {
|
||||||
return ShapeType.Rectangle
|
return ShapeType.Rectangle
|
||||||
},
|
},
|
||||||
|
firstSelectedShape(data) {
|
||||||
|
return getSelectedShapes(data)[0]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
conditions: {
|
conditions: {
|
||||||
isPointingCanvas(data, payload: PointerInfo) {
|
isPointingCanvas(data, payload: PointerInfo) {
|
||||||
|
@ -799,6 +864,9 @@ const state = createState({
|
||||||
isReadOnly(data) {
|
isReadOnly(data) {
|
||||||
return data.isReadOnly
|
return data.isReadOnly
|
||||||
},
|
},
|
||||||
|
canEditSelectedShape(data, payload, result: Shape) {
|
||||||
|
return getShapeUtils(result).canEdit
|
||||||
|
},
|
||||||
distanceImpliesDrag(data, payload: PointerInfo) {
|
distanceImpliesDrag(data, payload: PointerInfo) {
|
||||||
return vec.dist2(payload.origin, payload.point) > 8
|
return vec.dist2(payload.origin, payload.point) > 8
|
||||||
},
|
},
|
||||||
|
@ -842,6 +910,9 @@ const state = createState({
|
||||||
hasSelection(data) {
|
hasSelection(data) {
|
||||||
return getSelectedIds(data).size > 0
|
return getSelectedIds(data).size > 0
|
||||||
},
|
},
|
||||||
|
hasSingleSelection(data) {
|
||||||
|
return getSelectedIds(data).size === 1
|
||||||
|
},
|
||||||
hasMultipleSelection(data) {
|
hasMultipleSelection(data) {
|
||||||
return getSelectedIds(data).size > 1
|
return getSelectedIds(data).size > 1
|
||||||
},
|
},
|
||||||
|
@ -910,6 +981,14 @@ const state = createState({
|
||||||
session.clear()
|
session.clear()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Editing
|
||||||
|
startEditSession(data) {
|
||||||
|
session.current = new Sessions.EditSession(data)
|
||||||
|
},
|
||||||
|
updateEditSession(data, payload: { change: Partial<Shape> }) {
|
||||||
|
session.current.update(data, payload.change)
|
||||||
|
},
|
||||||
|
|
||||||
// Brushing
|
// Brushing
|
||||||
startBrushSession(data, payload: PointerInfo) {
|
startBrushSession(data, payload: PointerInfo) {
|
||||||
session.current = new Sessions.BrushSession(
|
session.current = new Sessions.BrushSession(
|
||||||
|
@ -1197,6 +1276,23 @@ const state = createState({
|
||||||
ungroupSelection(data) {
|
ungroupSelection(data) {
|
||||||
commands.ungroup(data)
|
commands.ungroup(data)
|
||||||
},
|
},
|
||||||
|
resetShapeBounds(data) {
|
||||||
|
commands.resetBounds(data)
|
||||||
|
},
|
||||||
|
|
||||||
|
/* --------------------- Editing -------------------- */
|
||||||
|
|
||||||
|
setEditingId(data) {
|
||||||
|
const selectedShape = getSelectedShapes(data)[0]
|
||||||
|
if (getShapeUtils(selectedShape).canEdit) {
|
||||||
|
data.editingId = selectedShape.id
|
||||||
|
}
|
||||||
|
|
||||||
|
getPageState(data).selectedIds = new Set([selectedShape.id])
|
||||||
|
},
|
||||||
|
clearEditingId(data) {
|
||||||
|
data.editingId = null
|
||||||
|
},
|
||||||
|
|
||||||
/* ---------------------- Tool ---------------------- */
|
/* ---------------------- Tool ---------------------- */
|
||||||
|
|
||||||
|
@ -1478,6 +1574,10 @@ const state = createState({
|
||||||
|
|
||||||
/* ---------------------- Data ---------------------- */
|
/* ---------------------- Data ---------------------- */
|
||||||
|
|
||||||
|
restoreSavedData(data) {
|
||||||
|
storage.firstLoad(data)
|
||||||
|
},
|
||||||
|
|
||||||
saveToFileSystem(data) {
|
saveToFileSystem(data) {
|
||||||
storage.saveToFileSystem(data)
|
storage.saveToFileSystem(data)
|
||||||
},
|
},
|
||||||
|
@ -1511,10 +1611,6 @@ const state = createState({
|
||||||
storage.saveToLocalStorage(data)
|
storage.saveToLocalStorage(data)
|
||||||
},
|
},
|
||||||
|
|
||||||
restoreSavedData(data) {
|
|
||||||
storage.firstLoad(data)
|
|
||||||
},
|
|
||||||
|
|
||||||
clearBoundsRotation(data) {
|
clearBoundsRotation(data) {
|
||||||
data.boundsRotation = 0
|
data.boundsRotation = 0
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as fa from 'browser-fs-access'
|
import * as fa from 'browser-fs-access'
|
||||||
import { Data, Page, PageState, TLDocument } from 'types'
|
import { Data, Page, PageState, TLDocument } from 'types'
|
||||||
import { lzw_decode, lzw_encode, setToArray } from 'utils/utils'
|
import { decompress, compress, setToArray } from 'utils/utils'
|
||||||
import state from './state'
|
import state from './state'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import { v4 as uuid } from 'uuid'
|
import { v4 as uuid } from 'uuid'
|
||||||
|
@ -66,7 +66,7 @@ class Storage {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoredData: any = JSON.parse(lzw_decode(savedData))
|
const restoredData: any = JSON.parse(decompress(savedData))
|
||||||
|
|
||||||
this.load(data, restoredData)
|
this.load(data, restoredData)
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ class Storage {
|
||||||
)
|
)
|
||||||
|
|
||||||
if (savedPage !== null) {
|
if (savedPage !== null) {
|
||||||
const restored: Page = JSON.parse(lzw_decode(savedPage))
|
const restored: Page = JSON.parse(decompress(savedPage))
|
||||||
dataToSave.document.pages[pageId] = restored
|
dataToSave.document.pages[pageId] = restored
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,7 +100,7 @@ class Storage {
|
||||||
// Save current data to local storage
|
// Save current data to local storage
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
storageId(fileId, 'document', fileId),
|
storageId(fileId, 'document', fileId),
|
||||||
lzw_encode(dataToSave)
|
compress(dataToSave)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -134,7 +134,7 @@ class Storage {
|
||||||
const page = data.document.pages[pageId]
|
const page = data.document.pages[pageId]
|
||||||
const json = JSON.stringify(page)
|
const json = JSON.stringify(page)
|
||||||
|
|
||||||
localStorage.setItem(storageId(fileId, 'page', pageId), lzw_encode(json))
|
localStorage.setItem(storageId(fileId, 'page', pageId), compress(json))
|
||||||
|
|
||||||
// Save page state
|
// Save page state
|
||||||
|
|
||||||
|
@ -166,7 +166,7 @@ class Storage {
|
||||||
const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
|
const savedPage = localStorage.getItem(storageId(fileId, 'page', pageId))
|
||||||
|
|
||||||
if (savedPage !== null) {
|
if (savedPage !== null) {
|
||||||
data.document.pages[pageId] = JSON.parse(lzw_decode(savedPage))
|
data.document.pages[pageId] = JSON.parse(decompress(savedPage))
|
||||||
} else {
|
} else {
|
||||||
data.document.pages[pageId] = {
|
data.document.pages[pageId] = {
|
||||||
id: pageId,
|
id: pageId,
|
||||||
|
|
|
@ -1 +1,7 @@
|
||||||
@import url("https://fonts.googleapis.com/css2?family=Recursive:wght@500;700&display=swap");
|
@import url('https://fonts.googleapis.com/css2?family=Recursive:wght@500;700&display=swap');
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Verveine Regular';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: normal;
|
||||||
|
src: local('Verveine Regular'), url('/VerveineRegular.woff') format('woff');
|
||||||
|
}
|
||||||
|
|
16
types.ts
16
types.ts
|
@ -18,12 +18,13 @@ export interface Data {
|
||||||
isToolLocked: boolean
|
isToolLocked: boolean
|
||||||
isPenLocked: boolean
|
isPenLocked: boolean
|
||||||
}
|
}
|
||||||
currentStyle: ShapeStyles
|
currentStyle: ShapeStyles & TextStyles
|
||||||
activeTool: ShapeType | 'select'
|
activeTool: ShapeType | 'select'
|
||||||
brush?: Bounds
|
brush?: Bounds
|
||||||
boundsRotation: number
|
boundsRotation: number
|
||||||
pointedId?: string
|
pointedId?: string
|
||||||
hoveredId?: string
|
hoveredId?: string
|
||||||
|
editingId?: string
|
||||||
currentPageId: string
|
currentPageId: string
|
||||||
currentParentId: string
|
currentParentId: string
|
||||||
currentCodeFileId: string
|
currentCodeFileId: string
|
||||||
|
@ -100,6 +101,13 @@ export enum DashStyle {
|
||||||
Dotted = 'Dotted',
|
Dotted = 'Dotted',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum FontSize {
|
||||||
|
Small = 'Small',
|
||||||
|
Medium = 'Medium',
|
||||||
|
Large = 'Large',
|
||||||
|
ExtraLarge = 'ExtraLarge',
|
||||||
|
}
|
||||||
|
|
||||||
export type ShapeStyles = {
|
export type ShapeStyles = {
|
||||||
color: ColorStyle
|
color: ColorStyle
|
||||||
size: SizeStyle
|
size: SizeStyle
|
||||||
|
@ -107,6 +115,10 @@ export type ShapeStyles = {
|
||||||
isFilled: boolean
|
isFilled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TextStyles = {
|
||||||
|
fontSize: FontSize
|
||||||
|
}
|
||||||
|
|
||||||
export interface BaseShape {
|
export interface BaseShape {
|
||||||
id: string
|
id: string
|
||||||
seed: number
|
seed: number
|
||||||
|
@ -182,6 +194,8 @@ export interface ArrowShape extends BaseShape {
|
||||||
export interface TextShape extends BaseShape {
|
export interface TextShape extends BaseShape {
|
||||||
type: ShapeType.Text
|
type: ShapeType.Text
|
||||||
text: string
|
text: string
|
||||||
|
size: number[] | 'auto'
|
||||||
|
fontSize: FontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupShape extends BaseShape {
|
export interface GroupShape extends BaseShape {
|
||||||
|
|
|
@ -1771,8 +1771,9 @@ export function getPoint(
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function lzw_encode(s: string) {
|
export function compress(s: string) {
|
||||||
return s
|
return s
|
||||||
|
|
||||||
const dict = {}
|
const dict = {}
|
||||||
const data = (s + '').split('')
|
const data = (s + '').split('')
|
||||||
|
|
||||||
|
@ -1805,7 +1806,7 @@ export function lzw_encode(s: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Decompress an LZW-encoded string
|
// Decompress an LZW-encoded string
|
||||||
export function lzw_decode(s: string) {
|
export function decompress(s: string) {
|
||||||
return s
|
return s
|
||||||
|
|
||||||
const dict = {}
|
const dict = {}
|
||||||
|
|
Loading…
Reference in a new issue