Adds text
This commit is contained in:
parent
098afdd180
commit
aabc7e8e0f
17 changed files with 250 additions and 133 deletions
|
@ -1,6 +1,6 @@
|
||||||
import styled from 'styles'
|
import styled from 'styles'
|
||||||
import state, { useSelector } from 'state'
|
import state, { useSelector } from 'state'
|
||||||
import React, { useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import useZoomEvents from 'hooks/useZoomEvents'
|
import useZoomEvents from 'hooks/useZoomEvents'
|
||||||
import useCamera from 'hooks/useCamera'
|
import useCamera from 'hooks/useCamera'
|
||||||
import Defs from './defs'
|
import Defs from './defs'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useRef, memo } from 'react'
|
import React, { useRef, memo, useEffect } from 'react'
|
||||||
import state, { 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'
|
||||||
|
@ -16,14 +16,21 @@ interface ShapeProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
const rGroup = useRef<SVGGElement>(null)
|
||||||
|
const rFocusable = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
const isEditing = useSelector((s) => s.data.editingId === id)
|
const isEditing = useSelector((s) => s.data.editingId === id)
|
||||||
|
|
||||||
const rGroup = useRef<SVGGElement>(null)
|
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
||||||
|
|
||||||
const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
|
const events = useShapeEvents(id, getShapeUtils(shape)?.isParent, rGroup)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isEditing) {
|
||||||
|
setTimeout(() => rFocusable.current?.focus(), 0)
|
||||||
|
}
|
||||||
|
}, [isEditing])
|
||||||
|
|
||||||
// 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
|
||||||
// a deleted shape will still be pulled here before the page component
|
// a deleted shape will still be pulled here before the page component
|
||||||
|
@ -32,6 +39,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
|
|
||||||
const style = getShapeStyle(shape.style)
|
const style = getShapeStyle(shape.style)
|
||||||
const shapeUtils = getShapeUtils(shape)
|
const shapeUtils = getShapeUtils(shape)
|
||||||
|
|
||||||
const { isShy, isParent, isForeignObject } = shapeUtils
|
const { isShy, isParent, isForeignObject } = shapeUtils
|
||||||
|
|
||||||
const bounds = shapeUtils.getBounds(shape)
|
const bounds = shapeUtils.getBounds(shape)
|
||||||
|
@ -45,11 +53,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
`
|
`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledGroup
|
<StyledGroup ref={rGroup} transform={transform}>
|
||||||
ref={rGroup}
|
|
||||||
transform={transform}
|
|
||||||
onBlur={() => state.send('BLURRED_SHAPE', { target: id })}
|
|
||||||
>
|
|
||||||
{isSelecting &&
|
{isSelecting &&
|
||||||
!isShy &&
|
!isShy &&
|
||||||
(isForeignObject ? (
|
(isForeignObject ? (
|
||||||
|
@ -73,7 +77,7 @@ function Shape({ id, isSelecting, parentPoint }: ShapeProps) {
|
||||||
|
|
||||||
{!shape.isHidden &&
|
{!shape.isHidden &&
|
||||||
(isForeignObject ? (
|
(isForeignObject ? (
|
||||||
shapeUtils.render(shape, { isEditing })
|
shapeUtils.render(shape, { isEditing, ref: rFocusable })
|
||||||
) : (
|
) : (
|
||||||
<RealShape
|
<RealShape
|
||||||
isParent={isParent}
|
isParent={isParent}
|
||||||
|
@ -107,7 +111,6 @@ const RealShape = memo(function RealShape({
|
||||||
id,
|
id,
|
||||||
style,
|
style,
|
||||||
isParent,
|
isParent,
|
||||||
isEditing,
|
|
||||||
}: RealShapeProps) {
|
}: RealShapeProps) {
|
||||||
return <StyledShape as="use" data-shy={isParent} href={'#' + id} {...style} />
|
return <StyledShape as="use" data-shy={isParent} href={'#' + id} {...style} />
|
||||||
})
|
})
|
||||||
|
|
|
@ -105,7 +105,7 @@ export default function ToolsPanel() {
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip label="Text">
|
<Tooltip label="Text">
|
||||||
<IconButton
|
<IconButton
|
||||||
name={ShapeType.Arrow}
|
name={ShapeType.Text}
|
||||||
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
bp={{ '@initial': 'mobile', '@sm': 'small' }}
|
||||||
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
|
size={{ '@initial': 'medium', '@sm': 'small', '@md': 'large' }}
|
||||||
onClick={selectTextTool}
|
onClick={selectTextTool}
|
||||||
|
|
31
decs.d.ts
vendored
Normal file
31
decs.d.ts
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
type CSSOMString = string
|
||||||
|
type FontFaceLoadStatus = 'unloaded' | 'loading' | 'loaded' | 'error'
|
||||||
|
type FontFaceSetStatus = 'loading' | 'loaded'
|
||||||
|
|
||||||
|
interface FontFace {
|
||||||
|
family: CSSOMString
|
||||||
|
style: CSSOMString
|
||||||
|
weight: CSSOMString
|
||||||
|
stretch: CSSOMString
|
||||||
|
unicodeRange: CSSOMString
|
||||||
|
variant: CSSOMString
|
||||||
|
featureSettings: CSSOMString
|
||||||
|
variationSettings: CSSOMString
|
||||||
|
display: CSSOMString
|
||||||
|
readonly status: FontFaceLoadStatus
|
||||||
|
readonly loaded: Promise<FontFace>
|
||||||
|
load(): Promise<FontFace>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FontFaceSet {
|
||||||
|
readonly status: FontFaceSetStatus
|
||||||
|
readonly ready: Promise<FontFaceSet>
|
||||||
|
check(font: string, text?: string): Boolean
|
||||||
|
load(font: string, text?: string): Promise<FontFace[]>
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Document {
|
||||||
|
fonts: FontFaceSet
|
||||||
|
}
|
||||||
|
}
|
|
@ -180,11 +180,7 @@ export default function useKeyboardEvents() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 't': {
|
case 't': {
|
||||||
if (metaKey(e)) {
|
state.send('SELECTED_TEXT_TOOL', getKeyboardEventInfo(e))
|
||||||
state.send('DUPLICATED', getKeyboardEventInfo(e))
|
|
||||||
} else {
|
|
||||||
state.send('SELECTED_DOT_TOOL', getKeyboardEventInfo(e))
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'c': {
|
case 'c': {
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import { useEffect } from "react"
|
import { useEffect } from 'react'
|
||||||
import state from "state"
|
import state from 'state'
|
||||||
|
|
||||||
export default function useLoadOnMount() {
|
export default function useLoadOnMount() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
state.send("MOUNTED")
|
const fonts = (document as any).fonts
|
||||||
|
|
||||||
|
fonts
|
||||||
|
.load('12px Verveine Regular', 'Fonts are loaded!')
|
||||||
|
.then(() => state.send('MOUNTED'))
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
state.send("UNMOUNTED")
|
state.send('UNMOUNTED')
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,10 +63,14 @@ export function getFontSize(size: FontSize) {
|
||||||
return fontSizes[size]
|
return fontSizes[size]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFontStyle(size: FontSize, style: ShapeStyles) {
|
export function getFontStyle(
|
||||||
|
size: FontSize,
|
||||||
|
scale: number,
|
||||||
|
style: ShapeStyles
|
||||||
|
) {
|
||||||
const fontSize = getFontSize(size)
|
const fontSize = getFontSize(size)
|
||||||
|
|
||||||
return `${fontSize}px Verveine Regular`
|
return `${fontSize * scale}px Verveine Regular`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getShapeStyle(
|
export function getShapeStyle(
|
||||||
|
|
|
@ -32,6 +32,7 @@ import draw from './draw'
|
||||||
import arrow from './arrow'
|
import arrow from './arrow'
|
||||||
import group from './group'
|
import group from './group'
|
||||||
import text from './text'
|
import text from './text'
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Shape Utiliies
|
Shape Utiliies
|
||||||
|
@ -173,9 +174,14 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
render(
|
render(
|
||||||
this: ShapeUtility<K>,
|
this: ShapeUtility<K>,
|
||||||
shape: K,
|
shape: K,
|
||||||
info: { isEditing: boolean }
|
info: {
|
||||||
|
isEditing: boolean
|
||||||
|
ref: React.MutableRefObject<HTMLTextAreaElement>
|
||||||
|
}
|
||||||
): JSX.Element
|
): JSX.Element
|
||||||
|
|
||||||
|
invalidate(this: ShapeUtility<K>, shape: K): ShapeUtility<K>
|
||||||
|
|
||||||
// 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
|
||||||
|
|
||||||
|
@ -191,7 +197,7 @@ 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
|
shouldDelete(this: ShapeUtility<K>, shape: K): boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
// A mapping of shape types to shape utilities.
|
// A mapping of shape types to shape utilities.
|
||||||
|
@ -350,9 +356,14 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
getShouldDelete(shape) {
|
shouldDelete(shape) {
|
||||||
return false
|
return false
|
||||||
},
|
},
|
||||||
|
|
||||||
|
invalidate(shape) {
|
||||||
|
this.boundsCache.delete(shape)
|
||||||
|
return this
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,14 @@ mdiv.style.whiteSpace = 'pre'
|
||||||
mdiv.style.width = 'auto'
|
mdiv.style.width = 'auto'
|
||||||
mdiv.style.border = '1px solid red'
|
mdiv.style.border = '1px solid red'
|
||||||
mdiv.style.padding = '4px'
|
mdiv.style.padding = '4px'
|
||||||
|
mdiv.style.lineHeight = '1'
|
||||||
mdiv.style.margin = '0px'
|
mdiv.style.margin = '0px'
|
||||||
mdiv.style.opacity = '0'
|
mdiv.style.opacity = '0'
|
||||||
mdiv.style.position = 'absolute'
|
mdiv.style.position = 'absolute'
|
||||||
mdiv.style.top = '-500px'
|
mdiv.style.top = '-500px'
|
||||||
mdiv.style.left = '0px'
|
mdiv.style.left = '0px'
|
||||||
mdiv.style.zIndex = '9999'
|
mdiv.style.zIndex = '9999'
|
||||||
|
mdiv.setAttribute('readonly', 'true')
|
||||||
document.body.appendChild(mdiv)
|
document.body.appendChild(mdiv)
|
||||||
|
|
||||||
const text = registerShapeUtils<TextShape>({
|
const text = registerShapeUtils<TextShape>({
|
||||||
|
@ -50,23 +52,20 @@ const text = registerShapeUtils<TextShape>({
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
style: defaultStyle,
|
style: defaultStyle,
|
||||||
text: '',
|
text: '',
|
||||||
|
scale: 1,
|
||||||
size: 'auto',
|
size: 'auto',
|
||||||
fontSize: FontSize.Medium,
|
fontSize: FontSize.Medium,
|
||||||
...props,
|
...props,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
render(shape, { isEditing }) {
|
render(shape, { isEditing, ref }) {
|
||||||
const { id, text, style } = shape
|
const { id, text, style } = shape
|
||||||
const styles = getShapeStyle(style)
|
const styles = getShapeStyle(style)
|
||||||
|
const font = getFontStyle(shape.fontSize, shape.scale, shape.style)
|
||||||
|
|
||||||
const font = getFontStyle(shape.fontSize, shape.style)
|
|
||||||
const bounds = this.getBounds(shape)
|
const bounds = this.getBounds(shape)
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
state.send('EDITED_SHAPE', { change: { text: e.currentTarget.value } })
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<foreignObject
|
<foreignObject
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -76,44 +75,63 @@ const text = registerShapeUtils<TextShape>({
|
||||||
height={bounds.height}
|
height={bounds.height}
|
||||||
pointerEvents="none"
|
pointerEvents="none"
|
||||||
>
|
>
|
||||||
<StyledText
|
{isEditing ? (
|
||||||
key={id}
|
<StyledTextArea
|
||||||
|
ref={ref}
|
||||||
style={{
|
style={{
|
||||||
font,
|
font,
|
||||||
color: styles.fill,
|
color: styles.stroke,
|
||||||
}}
|
}}
|
||||||
value={text}
|
value={text}
|
||||||
onChange={handleChange}
|
autoComplete="false"
|
||||||
isEditing={isEditing}
|
autoCapitalize="false"
|
||||||
onFocus={(e) => e.currentTarget.select()}
|
autoCorrect="false"
|
||||||
|
onFocus={(e) => {
|
||||||
|
e.currentTarget.select()
|
||||||
|
state.send('FOCUSED_EDITING_SHAPE')
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
state.send('BLURRED_EDITING_SHAPE')
|
||||||
|
}}
|
||||||
|
onChange={(e) => {
|
||||||
|
state.send('EDITED_SHAPE', {
|
||||||
|
change: { text: e.currentTarget.value },
|
||||||
|
})
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<StyledText
|
||||||
|
style={{
|
||||||
|
font,
|
||||||
|
color: styles.stroke,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</StyledText>
|
||||||
|
)}
|
||||||
</foreignObject>
|
</foreignObject>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
getBounds(shape) {
|
getBounds(shape) {
|
||||||
|
if (!this.boundsCache.has(shape)) {
|
||||||
|
mdiv.innerHTML = shape.text || ' ' // + ' '
|
||||||
|
mdiv.style.font = getFontStyle(shape.fontSize, shape.scale, shape.style)
|
||||||
|
|
||||||
const [minX, minY] = shape.point
|
const [minX, minY] = shape.point
|
||||||
let width: number
|
const [width, height] = [mdiv.offsetWidth, mdiv.offsetHeight]
|
||||||
let height: number
|
|
||||||
|
|
||||||
if (shape.size === 'auto') {
|
this.boundsCache.set(shape, {
|
||||||
// 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,
|
minX,
|
||||||
maxX: minX + width,
|
maxX: minX + width,
|
||||||
minY,
|
minY,
|
||||||
maxY: minY + height,
|
maxY: minY + height,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return this.boundsCache.get(shape)
|
||||||
},
|
},
|
||||||
|
|
||||||
hitTest(shape, test) {
|
hitTest(shape, test) {
|
||||||
|
@ -122,37 +140,25 @@ const text = registerShapeUtils<TextShape>({
|
||||||
|
|
||||||
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
|
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
|
||||||
if (shape.rotation === 0 && !shape.isAspectRatioLocked) {
|
if (shape.rotation === 0 && !shape.isAspectRatioLocked) {
|
||||||
shape.size = [bounds.width, bounds.height]
|
|
||||||
shape.point = [bounds.minX, bounds.minY]
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
shape.scale = initialShape.scale * Math.abs(scaleX)
|
||||||
} else {
|
} else {
|
||||||
if (initialShape.size === 'auto') return
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
|
||||||
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 =
|
shape.rotation =
|
||||||
(scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
|
(scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
|
||||||
? -initialShape.rotation
|
? -initialShape.rotation
|
||||||
: initialShape.rotation
|
: initialShape.rotation
|
||||||
|
|
||||||
|
shape.scale = initialShape.scale * Math.abs(Math.min(scaleX, scaleY))
|
||||||
}
|
}
|
||||||
|
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
transformSingle(shape, bounds) {
|
transformSingle(shape, bounds, { initialShape, scaleX }) {
|
||||||
shape.size = [bounds.width, bounds.height]
|
|
||||||
shape.point = [bounds.minX, bounds.minY]
|
shape.point = [bounds.minX, bounds.minY]
|
||||||
|
shape.scale = initialShape.scale * Math.abs(scaleX)
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -161,14 +167,14 @@ const text = registerShapeUtils<TextShape>({
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
getShouldDelete(shape) {
|
shouldDelete(shape) {
|
||||||
return shape.text.length === 0
|
return shape.text.length === 0
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
export default text
|
export default text
|
||||||
|
|
||||||
const StyledText = styled('textarea', {
|
const StyledText = styled('div', {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
|
@ -180,13 +186,21 @@ const StyledText = styled('textarea', {
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
userSelect: 'none',
|
||||||
|
})
|
||||||
|
|
||||||
variants: {
|
const StyledTextArea = styled('textarea', {
|
||||||
isEditing: {
|
width: '100%',
|
||||||
true: {
|
height: '100%',
|
||||||
|
border: 'none',
|
||||||
|
padding: '4px',
|
||||||
|
whiteSpace: 'pre',
|
||||||
|
resize: 'none',
|
||||||
|
minHeight: 1,
|
||||||
|
minWidth: 1,
|
||||||
|
outline: 'none',
|
||||||
|
overflow: 'hidden',
|
||||||
backgroundColor: '$boundsBg',
|
backgroundColor: '$boundsBg',
|
||||||
pointerEvents: 'all',
|
pointerEvents: 'all',
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default function handleCommand(
|
||||||
|
|
||||||
const shape = page.shapes[initialShape.id]
|
const shape = page.shapes[initialShape.id]
|
||||||
|
|
||||||
if (getShapeUtils(shape).getShouldDelete(shape)) {
|
if (getShapeUtils(shape).shouldDelete(shape)) {
|
||||||
delete page.shapes[initialShape.id]
|
delete page.shapes[initialShape.id]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,21 +15,20 @@ export default function transformCommand(
|
||||||
history.execute(
|
history.execute(
|
||||||
data,
|
data,
|
||||||
new Command({
|
new Command({
|
||||||
name: 'translate_shapes',
|
name: 'transform_shapes',
|
||||||
category: 'canvas',
|
category: 'canvas',
|
||||||
do(data, isInitial) {
|
do(data) {
|
||||||
const { type, shapeBounds } = after
|
const { type, shapeBounds } = after
|
||||||
|
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (let id in shapeBounds) {
|
for (let id in shapeBounds) {
|
||||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
const { initialShapeBounds: bounds } = after.shapeBounds[id]
|
||||||
shapeBounds[id]
|
const { initialShape, transformOrigin } = before.shapeBounds[id]
|
||||||
|
|
||||||
const shape = shapes[id]
|
const shape = shapes[id]
|
||||||
|
|
||||||
getShapeUtils(shape)
|
getShapeUtils(shape)
|
||||||
.transform(shape, initialShapeBounds, {
|
.transform(shape, bounds, {
|
||||||
type,
|
type,
|
||||||
initialShape,
|
initialShape,
|
||||||
transformOrigin,
|
transformOrigin,
|
||||||
|
@ -42,24 +41,11 @@ export default function transformCommand(
|
||||||
updateParents(data, Object.keys(shapeBounds))
|
updateParents(data, Object.keys(shapeBounds))
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
const { type, shapeBounds } = before
|
const { shapeBounds } = before
|
||||||
|
|
||||||
const { shapes } = getPage(data)
|
const { shapes } = getPage(data)
|
||||||
|
|
||||||
for (let id in shapeBounds) {
|
for (let id in shapeBounds) {
|
||||||
const { initialShape, initialShapeBounds, transformOrigin } =
|
shapes[id] = shapeBounds[id].initialShape
|
||||||
shapeBounds[id]
|
|
||||||
const shape = shapes[id]
|
|
||||||
|
|
||||||
getShapeUtils(shape)
|
|
||||||
.transform(shape, initialShapeBounds, {
|
|
||||||
type,
|
|
||||||
initialShape,
|
|
||||||
transformOrigin,
|
|
||||||
scaleX: scaleX < 0 ? scaleX * -1 : scaleX,
|
|
||||||
scaleY: scaleX < 0 ? scaleX * -1 : scaleX,
|
|
||||||
})
|
|
||||||
.onSessionComplete(shape)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateParents(data, Object.keys(shapeBounds))
|
updateParents(data, Object.keys(shapeBounds))
|
||||||
|
|
|
@ -5,6 +5,7 @@ import commands from 'state/commands'
|
||||||
import { current } from 'immer'
|
import { current } from 'immer'
|
||||||
import {
|
import {
|
||||||
getPage,
|
getPage,
|
||||||
|
getPageState,
|
||||||
getSelectedIds,
|
getSelectedIds,
|
||||||
getSelectedShapes,
|
getSelectedShapes,
|
||||||
getShape,
|
getShape,
|
||||||
|
|
|
@ -127,7 +127,8 @@ export function getTransformSnapshot(data: Data, transformType: Edge | Corner) {
|
||||||
const hasUnlockedShapes = initialShapes.length > 0
|
const hasUnlockedShapes = initialShapes.length > 0
|
||||||
|
|
||||||
const isAllAspectRatioLocked = initialShapes.every(
|
const isAllAspectRatioLocked = initialShapes.every(
|
||||||
(shape) => shape.isAspectRatioLocked
|
(shape) =>
|
||||||
|
shape.isAspectRatioLocked || !getShapeUtils(shape).canChangeAspectRatio
|
||||||
)
|
)
|
||||||
|
|
||||||
const shapesBounds = Object.fromEntries(
|
const shapesBounds = Object.fromEntries(
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
getSelectedIds,
|
getSelectedIds,
|
||||||
setSelectedIds,
|
setSelectedIds,
|
||||||
getPageState,
|
getPageState,
|
||||||
|
getShapes,
|
||||||
} from 'utils/utils'
|
} from 'utils/utils'
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
|
@ -117,12 +118,10 @@ const state = createState({
|
||||||
states: {
|
states: {
|
||||||
loading: {
|
loading: {
|
||||||
on: {
|
on: {
|
||||||
MOUNTED: [
|
MOUNTED: {
|
||||||
'restoreSavedData',
|
do: 'restoreSavedData',
|
||||||
{
|
|
||||||
to: 'ready',
|
to: 'ready',
|
||||||
},
|
},
|
||||||
],
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
ready: {
|
ready: {
|
||||||
|
@ -133,6 +132,7 @@ const state = createState({
|
||||||
else: ['zoomCameraToFit', 'zoomCameraToActual'],
|
else: ['zoomCameraToFit', 'zoomCameraToActual'],
|
||||||
},
|
},
|
||||||
on: {
|
on: {
|
||||||
|
LOADED_FONTS: 'resetShapes',
|
||||||
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: {
|
||||||
|
@ -206,6 +206,7 @@ const state = createState({
|
||||||
SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
|
SELECTED_LINE_TOOL: { unless: 'isReadOnly', to: 'line' },
|
||||||
SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
|
SELECTED_POLYLINE_TOOL: { unless: 'isReadOnly', to: 'polyline' },
|
||||||
SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
|
SELECTED_RECTANGLE_TOOL: { unless: 'isReadOnly', to: 'rectangle' },
|
||||||
|
SELECTED_TEXT_TOOL: { unless: 'isReadOnly', to: 'text' },
|
||||||
ZOOMED_CAMERA: {
|
ZOOMED_CAMERA: {
|
||||||
do: 'zoomCamera',
|
do: 'zoomCamera',
|
||||||
},
|
},
|
||||||
|
@ -430,8 +431,17 @@ const state = createState({
|
||||||
onExit: 'clearEditingId',
|
onExit: 'clearEditingId',
|
||||||
on: {
|
on: {
|
||||||
EDITED_SHAPE: { do: 'updateEditSession' },
|
EDITED_SHAPE: { do: 'updateEditSession' },
|
||||||
BLURRED_SHAPE: { do: 'completeSession', to: 'selecting' },
|
|
||||||
CANCELLED: { do: 'cancelSession', to: 'selecting' },
|
BLURRED_EDITING_SHAPE: { do: 'completeSession', to: 'selecting' },
|
||||||
|
CANCELLED: [
|
||||||
|
{
|
||||||
|
get: 'editingShape',
|
||||||
|
if: 'shouldDeleteShape',
|
||||||
|
do: 'breakSession',
|
||||||
|
else: 'cancelSession',
|
||||||
|
},
|
||||||
|
{ to: 'selecting' },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pinching: {
|
pinching: {
|
||||||
|
@ -730,6 +740,36 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
text: {
|
||||||
|
onEnter: 'setActiveToolText',
|
||||||
|
initial: 'creating',
|
||||||
|
states: {
|
||||||
|
creating: {
|
||||||
|
on: {
|
||||||
|
CANCELLED: { to: 'selecting' },
|
||||||
|
POINTED_SHAPE: [
|
||||||
|
{
|
||||||
|
get: 'newText',
|
||||||
|
do: 'createShape',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
get: 'firstSelectedShape',
|
||||||
|
if: 'canEditSelectedShape',
|
||||||
|
do: 'setEditingId',
|
||||||
|
to: 'editingShape',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
POINTED_CANVAS: [
|
||||||
|
{
|
||||||
|
get: 'newText',
|
||||||
|
do: 'createShape',
|
||||||
|
to: 'editingShape',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
ray: {
|
ray: {
|
||||||
onEnter: 'setActiveToolRay',
|
onEnter: 'setActiveToolRay',
|
||||||
initial: 'creating',
|
initial: 'creating',
|
||||||
|
@ -834,12 +874,6 @@ const state = createState({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
results: {
|
results: {
|
||||||
newArrow() {
|
|
||||||
return ShapeType.Arrow
|
|
||||||
},
|
|
||||||
newDraw() {
|
|
||||||
return ShapeType.Draw
|
|
||||||
},
|
|
||||||
newDot() {
|
newDot() {
|
||||||
return ShapeType.Dot
|
return ShapeType.Dot
|
||||||
},
|
},
|
||||||
|
@ -849,6 +883,15 @@ const state = createState({
|
||||||
newLine() {
|
newLine() {
|
||||||
return ShapeType.Line
|
return ShapeType.Line
|
||||||
},
|
},
|
||||||
|
newText() {
|
||||||
|
return ShapeType.Text
|
||||||
|
},
|
||||||
|
newDraw() {
|
||||||
|
return ShapeType.Draw
|
||||||
|
},
|
||||||
|
newArrow() {
|
||||||
|
return ShapeType.Arrow
|
||||||
|
},
|
||||||
newCircle() {
|
newCircle() {
|
||||||
return ShapeType.Circle
|
return ShapeType.Circle
|
||||||
},
|
},
|
||||||
|
@ -861,8 +904,14 @@ const state = createState({
|
||||||
firstSelectedShape(data) {
|
firstSelectedShape(data) {
|
||||||
return getSelectedShapes(data)[0]
|
return getSelectedShapes(data)[0]
|
||||||
},
|
},
|
||||||
|
editingShape(data) {
|
||||||
|
return getShape(data, data.editingId)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
conditions: {
|
conditions: {
|
||||||
|
shouldDeleteShape(data, payload, shape: Shape) {
|
||||||
|
return getShapeUtils(shape).shouldDelete(shape)
|
||||||
|
},
|
||||||
isPointingCanvas(data, payload: PointerInfo) {
|
isPointingCanvas(data, payload: PointerInfo) {
|
||||||
return payload.target === 'canvas'
|
return payload.target === 'canvas'
|
||||||
},
|
},
|
||||||
|
@ -959,6 +1008,13 @@ const state = createState({
|
||||||
},
|
},
|
||||||
|
|
||||||
/* --------------------- Shapes --------------------- */
|
/* --------------------- Shapes --------------------- */
|
||||||
|
resetShapes(data) {
|
||||||
|
const page = getPage(data)
|
||||||
|
Object.values(page.shapes).forEach((shape) => {
|
||||||
|
page.shapes[shape.id] = { ...shape }
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
createShape(data, payload, type: ShapeType) {
|
createShape(data, payload, type: ShapeType) {
|
||||||
const shape = createShape(type, {
|
const shape = createShape(type, {
|
||||||
parentId: data.currentPageId,
|
parentId: data.currentPageId,
|
||||||
|
@ -971,6 +1027,8 @@ const state = createState({
|
||||||
? siblings[siblings.length - 1].childIndex + 1
|
? siblings[siblings.length - 1].childIndex + 1
|
||||||
: 1
|
: 1
|
||||||
|
|
||||||
|
data.editingId = shape.id
|
||||||
|
|
||||||
getShapeUtils(shape).setProperty(shape, 'childIndex', childIndex)
|
getShapeUtils(shape).setProperty(shape, 'childIndex', childIndex)
|
||||||
|
|
||||||
getPage(data).shapes[shape.id] = shape
|
getPage(data).shapes[shape.id] = shape
|
||||||
|
@ -1344,6 +1402,9 @@ const state = createState({
|
||||||
setActiveToolLine(data) {
|
setActiveToolLine(data) {
|
||||||
data.activeTool = ShapeType.Line
|
data.activeTool = ShapeType.Line
|
||||||
},
|
},
|
||||||
|
setActiveToolText(data) {
|
||||||
|
data.activeTool = ShapeType.Text
|
||||||
|
},
|
||||||
|
|
||||||
/* --------------------- Camera --------------------- */
|
/* --------------------- Camera --------------------- */
|
||||||
|
|
||||||
|
@ -1777,3 +1838,6 @@ function getSelectionBounds(data: Data) {
|
||||||
|
|
||||||
return commonBounds
|
return commonBounds
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// state.enableLog(true)
|
||||||
|
// state.onUpdate((s) => console.log(s.log.filter((l) => l !== 'MOVED_POINTER')))
|
||||||
|
|
|
@ -5,7 +5,7 @@ import state from './state'
|
||||||
import { uniqueId } from 'utils/utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import * as idb from 'idb-keyval'
|
import * as idb from 'idb-keyval'
|
||||||
|
|
||||||
const CURRENT_VERSION = 'code_slate_0.0.7'
|
const CURRENT_VERSION = 'code_slate_0.0.8'
|
||||||
|
|
||||||
function storageId(fileId: string, label: string, id?: string) {
|
function storageId(fileId: string, label: string, id?: string) {
|
||||||
return [CURRENT_VERSION, fileId, label, id].filter(Boolean).join('_')
|
return [CURRENT_VERSION, fileId, label, id].filter(Boolean).join('_')
|
||||||
|
|
|
@ -16,6 +16,6 @@
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"baseUrl": "."
|
"baseUrl": "."
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "decs.d.ts"],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
1
types.ts
1
types.ts
|
@ -194,6 +194,7 @@ export interface TextShape extends BaseShape {
|
||||||
type: ShapeType.Text
|
type: ShapeType.Text
|
||||||
text: string
|
text: string
|
||||||
size: number[] | 'auto'
|
size: number[] | 'auto'
|
||||||
|
scale: number
|
||||||
fontSize: FontSize
|
fontSize: FontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue