Adds more tests, simplifies draw tool
This commit is contained in:
parent
75e60d5eb2
commit
ff58073d12
17 changed files with 521 additions and 20437 deletions
File diff suppressed because it is too large
Load diff
118
__tests__/__snapshots__/delete.test.ts.snap
Normal file
118
__tests__/__snapshots__/delete.test.ts.snap
Normal file
|
@ -0,0 +1,118 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`deletes and restores grouped shapes creates a group: data after mount from file 1`] = `
|
||||
Object {
|
||||
"code": Object {
|
||||
"file0": Object {
|
||||
"code": "",
|
||||
"id": "file0",
|
||||
"name": "index.ts",
|
||||
},
|
||||
},
|
||||
"id": "0001",
|
||||
"name": "My Document",
|
||||
"pages": Object {
|
||||
"page1": Object {
|
||||
"childIndex": 0,
|
||||
"id": "page1",
|
||||
"name": "Page 1",
|
||||
"shapes": Object {
|
||||
"1f6c251c-e12e-40b4-8dd2-c1847d80b72f": Object {
|
||||
"childIndex": 24,
|
||||
"id": "1f6c251c-e12e-40b4-8dd2-c1847d80b72f",
|
||||
"isAspectRatioLocked": false,
|
||||
"isGenerated": false,
|
||||
"isHidden": false,
|
||||
"isLocked": false,
|
||||
"name": "Rectangle",
|
||||
"parentId": "page1",
|
||||
"point": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
"radius": 2,
|
||||
"rotation": 0,
|
||||
"seed": 0.6440313303074272,
|
||||
"size": Array [
|
||||
67.22075383450237,
|
||||
72.92795609221832,
|
||||
],
|
||||
"style": Object {
|
||||
"color": "Black",
|
||||
"dash": "Solid",
|
||||
"isFilled": false,
|
||||
"size": "Small",
|
||||
},
|
||||
"type": "rectangle",
|
||||
},
|
||||
"5ca167d7-54de-47c9-aa8f-86affa25e44d": Object {
|
||||
"bend": 0,
|
||||
"childIndex": 16,
|
||||
"decorations": Object {
|
||||
"end": null,
|
||||
"middle": null,
|
||||
"start": null,
|
||||
},
|
||||
"handles": Object {
|
||||
"bend": Object {
|
||||
"id": "bend",
|
||||
"index": 2,
|
||||
"point": Array [
|
||||
3.2518097616315345,
|
||||
140.54510317291172,
|
||||
],
|
||||
},
|
||||
"end": Object {
|
||||
"id": "end",
|
||||
"index": 1,
|
||||
"point": Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
},
|
||||
"start": Object {
|
||||
"id": "start",
|
||||
"index": 0,
|
||||
"point": Array [
|
||||
6.503619523263069,
|
||||
281.09020634582345,
|
||||
],
|
||||
},
|
||||
},
|
||||
"id": "5ca167d7-54de-47c9-aa8f-86affa25e44d",
|
||||
"isAspectRatioLocked": false,
|
||||
"isGenerated": false,
|
||||
"isHidden": false,
|
||||
"isLocked": false,
|
||||
"name": "Arrow",
|
||||
"parentId": "page1",
|
||||
"point": Array [
|
||||
100,
|
||||
100,
|
||||
],
|
||||
"points": Array [
|
||||
Array [
|
||||
6.503619523263069,
|
||||
281.09020634582345,
|
||||
],
|
||||
Array [
|
||||
0,
|
||||
0,
|
||||
],
|
||||
],
|
||||
"rotation": 0,
|
||||
"seed": 0.08116783083496548,
|
||||
"style": Object {
|
||||
"color": "Black",
|
||||
"dash": "Solid",
|
||||
"isFilled": false,
|
||||
"size": "Small",
|
||||
},
|
||||
"type": "arrow",
|
||||
},
|
||||
},
|
||||
"type": "page",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
File diff suppressed because it is too large
Load diff
|
@ -1,22 +1,33 @@
|
|||
import state from 'state'
|
||||
import inputs from 'state/inputs'
|
||||
import { ShapeType } from 'types'
|
||||
import { getShape } from 'utils'
|
||||
import { idsAreSelected, point, rectangleId } from './test-utils'
|
||||
import {
|
||||
idsAreSelected,
|
||||
point,
|
||||
rectangleId,
|
||||
arrowId,
|
||||
getOnlySelectedShape,
|
||||
assertShapeProps,
|
||||
} from './test-utils'
|
||||
import * as json from './__mocks__/document.json'
|
||||
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
describe('deleting single shapes', () => {
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
|
||||
describe('selection', () => {
|
||||
it('deletes a shape and undoes the delete', () => {
|
||||
state
|
||||
.send('CANCELED')
|
||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
||||
.send('DELETED')
|
||||
|
||||
expect(getShape(state.data, rectangleId)).toBe(undefined)
|
||||
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
|
||||
state.send('DELETED')
|
||||
|
||||
expect(idsAreSelected(state.data, [])).toBe(true)
|
||||
expect(getShape(state.data, rectangleId)).toBe(undefined)
|
||||
|
||||
state.send('UNDO')
|
||||
|
||||
|
@ -26,6 +37,123 @@ describe('selection', () => {
|
|||
state.send('REDO')
|
||||
|
||||
expect(getShape(state.data, rectangleId)).toBe(undefined)
|
||||
expect(idsAreSelected(state.data, [])).toBe(true)
|
||||
|
||||
state.send('UNDO')
|
||||
})
|
||||
})
|
||||
|
||||
describe('deletes and restores grouped shapes', () => {
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
|
||||
it('creates a group', () => {
|
||||
expect(state.data.document).toMatchSnapshot('data after mount from file')
|
||||
|
||||
state
|
||||
.send('CANCELED')
|
||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
||||
.send(
|
||||
'POINTED_SHAPE',
|
||||
inputs.pointerDown(point({ shiftKey: true }), arrowId)
|
||||
)
|
||||
.send(
|
||||
'STOPPED_POINTING',
|
||||
inputs.pointerUp(point({ shiftKey: true }), arrowId)
|
||||
)
|
||||
|
||||
expect(idsAreSelected(state.data, [rectangleId, arrowId])).toBe(true)
|
||||
|
||||
state.send('GROUPED')
|
||||
|
||||
const group = getOnlySelectedShape(state.data)
|
||||
|
||||
// Should select the group
|
||||
expect(assertShapeProps(group, { type: ShapeType.Group }))
|
||||
|
||||
const arrow = getShape(state.data, arrowId)
|
||||
|
||||
// The arrow should be have the group as its parent
|
||||
expect(assertShapeProps(arrow, { parentId: group.id }))
|
||||
})
|
||||
|
||||
// it('selects the new group', () => {
|
||||
// expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
||||
// })
|
||||
|
||||
// it('assigns a new parent', () => {
|
||||
// expect(groupId === state.data.currentPageId).toBe(false)
|
||||
// })
|
||||
|
||||
// // Rectangle has the same new parent?
|
||||
// it('assigns new parent to all selected shapes', () => {
|
||||
// expect(hasParent(state.data, arrowId, groupId)).toBe(true)
|
||||
// })
|
||||
|
||||
// // New parent is selected?
|
||||
// it('selects the new parent', () => {
|
||||
// expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
||||
// })
|
||||
})
|
||||
|
||||
// // it('selects the group when pointing a shape', () => {
|
||||
// // state
|
||||
// // .send('CANCELED')
|
||||
// // .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
||||
// // .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
||||
|
||||
// // expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
||||
// // })
|
||||
|
||||
// // it('keeps selection when pointing bounds', () => {
|
||||
// // state
|
||||
// // .send('CANCELED')
|
||||
// // .send('POINTED_BOUNDS', inputs.pointerDown(point(), 'bounds'))
|
||||
// // .send('STOPPED_POINTING', inputs.pointerUp(point(), 'bounds'))
|
||||
|
||||
// // expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
||||
// // })
|
||||
|
||||
// // it('selects a grouped shape by double-pointing', () => {
|
||||
// // state
|
||||
// // .send('CANCELED')
|
||||
// // .send('DOUBLE_POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
||||
// // .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
||||
|
||||
// // expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
// // })
|
||||
|
||||
// // it('selects a sibling on point when selecting a grouped shape', () => {
|
||||
// // state
|
||||
// // .send('POINTED_SHAPE', inputs.pointerDown(point(), arrowId))
|
||||
// // .send('STOPPED_POINTING', inputs.pointerUp(point(), arrowId))
|
||||
|
||||
// // expect(idsAreSelected(state.data, [arrowId])).toBe(true)
|
||||
// // })
|
||||
|
||||
// // it('rises up a selection level when escape is pressed', () => {
|
||||
// // state
|
||||
// // .send('CANCELED')
|
||||
// // .send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
||||
// // .send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
||||
|
||||
// // expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
||||
// // })
|
||||
|
||||
// // it('deletes and restores one shape', () => {
|
||||
// // // Delete the rectangle first
|
||||
// // state.send('UNDO')
|
||||
|
||||
// // expect(getShape(state.data, rectangleId)).toBeTruthy()
|
||||
// // expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
|
||||
// // state.send('REDO')
|
||||
|
||||
// // expect(getShape(state.data, rectangleId)).toBe(undefined)
|
||||
|
||||
// // state.send('UNDO')
|
||||
|
||||
// // expect(getShape(state.data, rectangleId)).toBeTruthy()
|
||||
// // expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
// // })
|
||||
// })
|
||||
|
|
|
@ -7,7 +7,6 @@ describe('project', () => {
|
|||
|
||||
it('mounts the state', () => {
|
||||
state.send('MOUNTED')
|
||||
expect(state.data.document).toMatchSnapshot('data after initial mount')
|
||||
expect(state.isIn('ready')).toBe(true)
|
||||
})
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Data } from 'types'
|
||||
import { getSelectedIds } from 'utils'
|
||||
import { Data, Shape, ShapeType } from 'types'
|
||||
import { getSelectedIds, getSelectedShapes, getShape } from 'utils'
|
||||
|
||||
export const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
|
||||
export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
|
||||
|
@ -47,10 +47,43 @@ export function idsAreSelected(
|
|||
)
|
||||
}
|
||||
|
||||
export async function asyncDelay<T>(fn: () => T): Promise<T> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve(fn())
|
||||
}, 100)
|
||||
})
|
||||
export function hasParent(
|
||||
data: Data,
|
||||
childId: string,
|
||||
parentId: string
|
||||
): boolean {
|
||||
return getShape(data, childId).parentId === parentId
|
||||
}
|
||||
|
||||
export function getOnlySelectedShape(data: Data): Shape {
|
||||
const selectedShapes = getSelectedShapes(data)
|
||||
return selectedShapes.length === 1 ? selectedShapes[0] : undefined
|
||||
}
|
||||
|
||||
export function assertShapeType(
|
||||
data: Data,
|
||||
shapeId: string,
|
||||
type: ShapeType
|
||||
): boolean {
|
||||
const shape = getShape(data, shapeId)
|
||||
if (shape.type !== type) {
|
||||
throw new TypeError(
|
||||
`expected shape ${shapeId} to be of type ${type}, found ${shape?.type} instead`
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function assertShapeProps<T extends Shape>(
|
||||
shape: T,
|
||||
props: { [K in keyof Partial<T>]: T[K] }
|
||||
): boolean {
|
||||
for (const key in props) {
|
||||
if (shape[key] !== props[key]) {
|
||||
throw new TypeError(
|
||||
`expected shape ${shape.id} to have property ${key}: ${props[key]}, found ${key}: ${shape[key]} instead`
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import React, { memo } from 'react'
|
|||
import { useSelector } from 'state'
|
||||
import { deepCompareArrays, getCurrentCamera, getPage } from 'utils'
|
||||
import { DotCircle, Handle } from './misc'
|
||||
import useShapeDef from 'hooks/useShape'
|
||||
|
||||
export default function Defs(): JSX.Element {
|
||||
const zoom = useSelector((s) => getCurrentCamera(s.data).zoom)
|
||||
|
@ -31,7 +32,7 @@ export default function Defs(): JSX.Element {
|
|||
}
|
||||
|
||||
const Def = memo(function Def({ id }: { id: string }) {
|
||||
const shape = useSelector((s) => getPage(s.data).shapes[id])
|
||||
const shape = useShapeDef(id)
|
||||
|
||||
if (!shape) return null
|
||||
|
||||
|
|
|
@ -24,21 +24,23 @@ export default function useKeyboardEvents() {
|
|||
e.preventDefault()
|
||||
}
|
||||
|
||||
const info = getKeyboardEventInfo(e)
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowUp': {
|
||||
state.send('NUDGED', { delta: [0, -1], ...getKeyboardEventInfo(e) })
|
||||
state.send('NUDGED', { delta: [0, -1], ...info })
|
||||
break
|
||||
}
|
||||
case 'ArrowRight': {
|
||||
state.send('NUDGED', { delta: [1, 0], ...getKeyboardEventInfo(e) })
|
||||
state.send('NUDGED', { delta: [1, 0], ...info })
|
||||
break
|
||||
}
|
||||
case 'ArrowDown': {
|
||||
state.send('NUDGED', { delta: [0, 1], ...getKeyboardEventInfo(e) })
|
||||
state.send('NUDGED', { delta: [0, 1], ...info })
|
||||
break
|
||||
}
|
||||
case 'ArrowLeft': {
|
||||
state.send('NUDGED', { delta: [-1, 0], ...getKeyboardEventInfo(e) })
|
||||
state.send('NUDGED', { delta: [-1, 0], ...info })
|
||||
break
|
||||
}
|
||||
case '=': {
|
||||
|
@ -81,9 +83,9 @@ export default function useKeyboardEvents() {
|
|||
case 'z': {
|
||||
if (metaKey(e)) {
|
||||
if (e.shiftKey) {
|
||||
state.send('REDO', getKeyboardEventInfo(e))
|
||||
state.send('REDO', info)
|
||||
} else {
|
||||
state.send('UNDO', getKeyboardEventInfo(e))
|
||||
state.send('UNDO', info)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
@ -91,7 +93,7 @@ export default function useKeyboardEvents() {
|
|||
case '‘': {
|
||||
if (metaKey(e)) {
|
||||
state.send('MOVED', {
|
||||
...getKeyboardEventInfo(e),
|
||||
...info,
|
||||
type: MoveType.ToFront,
|
||||
})
|
||||
}
|
||||
|
@ -100,7 +102,7 @@ export default function useKeyboardEvents() {
|
|||
case '“': {
|
||||
if (metaKey(e)) {
|
||||
state.send('MOVED', {
|
||||
...getKeyboardEventInfo(e),
|
||||
...info,
|
||||
type: MoveType.ToBack,
|
||||
})
|
||||
}
|
||||
|
@ -109,7 +111,7 @@ export default function useKeyboardEvents() {
|
|||
case ']': {
|
||||
if (metaKey(e)) {
|
||||
state.send('MOVED', {
|
||||
...getKeyboardEventInfo(e),
|
||||
...info,
|
||||
type: MoveType.Forward,
|
||||
})
|
||||
}
|
||||
|
@ -118,30 +120,34 @@ export default function useKeyboardEvents() {
|
|||
case '[': {
|
||||
if (metaKey(e)) {
|
||||
state.send('MOVED', {
|
||||
...getKeyboardEventInfo(e),
|
||||
...info,
|
||||
type: MoveType.Backward,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'Shift': {
|
||||
state.send('PRESSED_SHIFT_KEY', getKeyboardEventInfo(e))
|
||||
state.send('PRESSED_SHIFT_KEY', info)
|
||||
break
|
||||
}
|
||||
case 'Alt': {
|
||||
state.send('PRESSED_ALT_KEY', getKeyboardEventInfo(e))
|
||||
state.send('PRESSED_ALT_KEY', info)
|
||||
break
|
||||
}
|
||||
case 'Backspace': {
|
||||
state.send('DELETED', getKeyboardEventInfo(e))
|
||||
if (metaKey(e)) {
|
||||
state.send('RESET_PAGE', info)
|
||||
} else {
|
||||
state.send('DELETED', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'g': {
|
||||
if (metaKey(e)) {
|
||||
if (e.shiftKey) {
|
||||
state.send('UNGROUPED', getKeyboardEventInfo(e))
|
||||
state.send('UNGROUPED', info)
|
||||
} else {
|
||||
state.send('GROUPED', getKeyboardEventInfo(e))
|
||||
state.send('GROUPED', info)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
@ -149,9 +155,9 @@ export default function useKeyboardEvents() {
|
|||
case 's': {
|
||||
if (metaKey(e)) {
|
||||
if (e.shiftKey) {
|
||||
state.send('SAVED_AS_TO_FILESYSTEM', getKeyboardEventInfo(e))
|
||||
state.send('SAVED_AS_TO_FILESYSTEM', info)
|
||||
} else {
|
||||
state.send('SAVED', getKeyboardEventInfo(e))
|
||||
state.send('SAVED', info)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
@ -160,47 +166,47 @@ export default function useKeyboardEvents() {
|
|||
if (metaKey(e)) {
|
||||
break
|
||||
} else {
|
||||
state.send('SELECTED_DOT_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_DOT_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'v': {
|
||||
if (metaKey(e)) {
|
||||
state.send('PASTED', getKeyboardEventInfo(e))
|
||||
state.send('PASTED', info)
|
||||
} else {
|
||||
state.send('SELECTED_SELECT_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_SELECT_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'a': {
|
||||
if (metaKey(e)) {
|
||||
state.send('SELECTED_ALL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_ALL', info)
|
||||
} else {
|
||||
state.send('SELECTED_ARROW_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_ARROW_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'd': {
|
||||
if (metaKey(e)) {
|
||||
state.send('DUPLICATED', getKeyboardEventInfo(e))
|
||||
state.send('DUPLICATED', info)
|
||||
} else {
|
||||
state.send('SELECTED_DRAW_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_DRAW_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 't': {
|
||||
state.send('SELECTED_TEXT_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_TEXT_TOOL', info)
|
||||
break
|
||||
}
|
||||
case 'c': {
|
||||
if (metaKey(e)) {
|
||||
if (e.shiftKey) {
|
||||
state.send('COPIED_TO_SVG', getKeyboardEventInfo(e))
|
||||
state.send('COPIED_TO_SVG', info)
|
||||
} else {
|
||||
state.send('COPIED', getKeyboardEventInfo(e))
|
||||
state.send('COPIED', info)
|
||||
}
|
||||
} else {
|
||||
state.send('SELECTED_ELLIPSE_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_ELLIPSE_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -208,17 +214,17 @@ export default function useKeyboardEvents() {
|
|||
if (metaKey(e)) {
|
||||
break
|
||||
} else {
|
||||
state.send('SELECTED_CIRCLE_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_CIRCLE_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'l': {
|
||||
if (metaKey(e)) {
|
||||
if (e.shiftKey) {
|
||||
state.send('LOADED_FROM_FILE_STSTEM', getKeyboardEventInfo(e))
|
||||
state.send('LOADED_FROM_FILE_STSTEM', info)
|
||||
}
|
||||
} else {
|
||||
state.send('SELECTED_LINE_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_LINE_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -226,7 +232,7 @@ export default function useKeyboardEvents() {
|
|||
if (metaKey(e)) {
|
||||
break
|
||||
} else {
|
||||
state.send('SELECTED_RAY_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_RAY_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -234,7 +240,7 @@ export default function useKeyboardEvents() {
|
|||
if (metaKey(e)) {
|
||||
break
|
||||
} else {
|
||||
state.send('SELECTED_POLYLINE_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_POLYLINE_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -242,7 +248,7 @@ export default function useKeyboardEvents() {
|
|||
if (metaKey(e)) {
|
||||
break
|
||||
} else {
|
||||
state.send('SELECTED_RECTANGLE_TOOL', getKeyboardEventInfo(e))
|
||||
state.send('SELECTED_RECTANGLE_TOOL', info)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -251,21 +257,23 @@ export default function useKeyboardEvents() {
|
|||
break
|
||||
}
|
||||
default: {
|
||||
state.send('PRESSED_KEY', getKeyboardEventInfo(e))
|
||||
state.send('PRESSED_KEY', info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyUp(e: KeyboardEvent) {
|
||||
const info = getKeyboardEventInfo(e)
|
||||
|
||||
if (e.key === 'Shift') {
|
||||
state.send('RELEASED_SHIFT_KEY', getKeyboardEventInfo(e))
|
||||
state.send('RELEASED_SHIFT_KEY', info)
|
||||
}
|
||||
|
||||
if (e.key === 'Alt') {
|
||||
state.send('RELEASED_ALT_KEY', getKeyboardEventInfo(e))
|
||||
state.send('RELEASED_ALT_KEY', info)
|
||||
}
|
||||
|
||||
state.send('RELEASED_KEY', getKeyboardEventInfo(e))
|
||||
state.send('RELEASED_KEY', info)
|
||||
}
|
||||
|
||||
document.body.addEventListener('keydown', handleKeyDown)
|
||||
|
|
20
hooks/useShape.ts
Normal file
20
hooks/useShape.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { useSelector } from 'state'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import { getShape } from 'utils'
|
||||
|
||||
export default function useShapeDef(id: string) {
|
||||
return useSelector(
|
||||
(s) => getShape(s.data, id),
|
||||
(prev, next) => {
|
||||
const shouldSkip = !(
|
||||
prev &&
|
||||
next &&
|
||||
next !== prev &&
|
||||
getShapeUtils(next).shouldRender(next, prev)
|
||||
)
|
||||
|
||||
return shouldSkip
|
||||
}
|
||||
)
|
||||
}
|
|
@ -12,7 +12,7 @@
|
|||
"start": "next start",
|
||||
"test-all": "yarn lint && yarn type-check && yarn test",
|
||||
"test:update": "jest --updateSnapshot",
|
||||
"test:watch": "jest --watchAll",
|
||||
"test:watch": "jest --watchAll --verbose=false --silent=false",
|
||||
"test": "jest --watchAll=false",
|
||||
"type-check": "tsc --pretty --noEmit"
|
||||
},
|
||||
|
@ -92,4 +92,4 @@
|
|||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data } from 'types'
|
||||
import { Data, Shape } from 'types'
|
||||
import {
|
||||
deepClone,
|
||||
getDocumentBranch,
|
||||
|
@ -17,16 +17,16 @@ export default function deleteSelected(data: Data): void {
|
|||
.filter((shape) => !shape.isLocked)
|
||||
.map((shape) => shape.id)
|
||||
|
||||
const page = getPage(data)
|
||||
|
||||
const childrenToDelete = selectedIdsArr
|
||||
.flatMap((id) => getDocumentBranch(data, id))
|
||||
.map((id) => deepClone(page.shapes[id]))
|
||||
const shapeIdsToDelete = selectedIdsArr.flatMap((id) =>
|
||||
getDocumentBranch(data, id)
|
||||
)
|
||||
|
||||
const remainingIds = selectedShapes
|
||||
.filter((shape) => shape.isLocked)
|
||||
.map((shape) => shape.id)
|
||||
|
||||
let deletedShapes: Shape[] = []
|
||||
|
||||
history.execute(
|
||||
data,
|
||||
new Command({
|
||||
|
@ -34,57 +34,83 @@ export default function deleteSelected(data: Data): void {
|
|||
category: 'canvas',
|
||||
manualSelection: true,
|
||||
do(data) {
|
||||
const page = getPage(data)
|
||||
|
||||
for (const id of selectedIdsArr) {
|
||||
const shape = page.shapes[id]
|
||||
if (!shape) {
|
||||
console.error('no shape ' + id)
|
||||
continue
|
||||
}
|
||||
|
||||
if (shape.parentId !== data.currentPageId) {
|
||||
const parent = page.shapes[shape.parentId]
|
||||
getShapeUtils(parent)
|
||||
.setProperty(
|
||||
parent,
|
||||
'children',
|
||||
parent.children.filter((childId) => childId !== shape.id)
|
||||
)
|
||||
.onChildrenChange(
|
||||
parent,
|
||||
parent.children.map((id) => page.shapes[id])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
for (const shape of childrenToDelete) {
|
||||
delete page.shapes[shape.id]
|
||||
}
|
||||
|
||||
// Update selected ids
|
||||
setSelectedIds(data, remainingIds)
|
||||
|
||||
// Recursively delete shapes (and maybe their parents too)
|
||||
deletedShapes = deleteShapes(data, shapeIdsToDelete)
|
||||
},
|
||||
undo(data) {
|
||||
const page = getPage(data)
|
||||
|
||||
for (const shape of childrenToDelete) {
|
||||
page.shapes[shape.id] = shape
|
||||
}
|
||||
|
||||
for (const shape of childrenToDelete) {
|
||||
if (shape.parentId !== data.currentPageId) {
|
||||
const parent = page.shapes[shape.parentId]
|
||||
getShapeUtils(parent)
|
||||
.setProperty(parent, 'children', [...parent.children, shape.id])
|
||||
.onChildrenChange(
|
||||
parent,
|
||||
parent.children.map((id) => page.shapes[id])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update selected ids
|
||||
setSelectedIds(data, selectedIdsArr)
|
||||
|
||||
// Restore deleted shapes
|
||||
deletedShapes.forEach((shape) => (page.shapes[shape.id] = shape))
|
||||
|
||||
// Update parents
|
||||
deletedShapes.forEach((shape) => {
|
||||
if (shape.parentId === data.currentPageId) return
|
||||
|
||||
const parent = page.shapes[shape.parentId]
|
||||
|
||||
getShapeUtils(parent)
|
||||
.setProperty(parent, 'children', [...parent.children, shape.id])
|
||||
.onChildrenChange(
|
||||
parent,
|
||||
parent.children.map((id) => page.shapes[id])
|
||||
)
|
||||
})
|
||||
},
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
/** Recursively delete shapes and their parents */
|
||||
|
||||
function deleteShapes(
|
||||
data: Data,
|
||||
shapeIds: string[],
|
||||
shapesDeleted: Shape[] = []
|
||||
): Shape[] {
|
||||
const parentsToDelete: string[] = []
|
||||
|
||||
const page = getPage(data)
|
||||
|
||||
const parentIds = new Set(shapeIds.map((id) => page.shapes[id].parentId))
|
||||
|
||||
// Delete shapes
|
||||
shapeIds.forEach((id) => {
|
||||
shapesDeleted.push(deepClone(page.shapes[id]))
|
||||
delete page.shapes[id]
|
||||
})
|
||||
|
||||
// Update parents
|
||||
parentIds.forEach((id) => {
|
||||
const parent = page.shapes[id]
|
||||
|
||||
if (!parent || id === page.id) return
|
||||
|
||||
getShapeUtils(parent)
|
||||
.setProperty(
|
||||
parent,
|
||||
'children',
|
||||
parent.children.filter((childId) => !shapeIds.includes(childId))
|
||||
)
|
||||
.onChildrenChange(
|
||||
parent,
|
||||
parent.children.map((id) => page.shapes[id])
|
||||
)
|
||||
|
||||
if (getShapeUtils(parent).shouldDelete(parent)) {
|
||||
parentsToDelete.push(parent.id)
|
||||
}
|
||||
})
|
||||
|
||||
if (parentsToDelete.length > 0) {
|
||||
return deleteShapes(data, parentsToDelete, shapesDeleted)
|
||||
}
|
||||
|
||||
return shapesDeleted
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { current } from 'immer'
|
|||
import { Data, DrawShape } from 'types'
|
||||
import BaseSession from './base-session'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import { getPage, getShape, updateParents } from 'utils'
|
||||
import { getBoundsFromPoints, getPage, getShape, updateParents } from 'utils'
|
||||
import vec from 'utils/vec'
|
||||
import commands from 'state/commands'
|
||||
export default class BrushSession extends BaseSession {
|
||||
|
@ -14,13 +14,12 @@ export default class BrushSession extends BaseSession {
|
|||
isLocked: boolean
|
||||
lockedDirection: 'horizontal' | 'vertical'
|
||||
|
||||
constructor(data: Data, id: string, point: number[], isLocked = false) {
|
||||
constructor(data: Data, id: string, point: number[]) {
|
||||
super(data)
|
||||
this.origin = point
|
||||
this.previous = point
|
||||
this.last = point
|
||||
this.snapshot = getDrawSnapshot(data, id)
|
||||
isLocked
|
||||
|
||||
// Add a first point but don't update the shape yet. We'll update
|
||||
// when the draw session ends; if the user hasn't added additional
|
||||
|
@ -42,29 +41,30 @@ export default class BrushSession extends BaseSession {
|
|||
): void => {
|
||||
const { snapshot } = this
|
||||
|
||||
const delta = vec.vec(this.origin, point)
|
||||
|
||||
// Drawing while holding shift will "lock" the pen to either the
|
||||
// x or y axis, depending on which direction has the greater
|
||||
// delta. Pressing shift will also add more points to "return"
|
||||
// the pen to the axis.
|
||||
if (isLocked) {
|
||||
if (!this.isLocked && this.points.length > 1) {
|
||||
this.isLocked = true
|
||||
const returning = [...this.previous]
|
||||
const bounds = getBoundsFromPoints(this.points)
|
||||
if (bounds.width > 8 || bounds.height > 8) {
|
||||
this.isLocked = true
|
||||
const returning = [...this.previous]
|
||||
|
||||
const isVertical = Math.abs(delta[0]) < Math.abs(delta[1])
|
||||
const isVertical = bounds.height > 8
|
||||
|
||||
if (isVertical) {
|
||||
this.lockedDirection = 'vertical'
|
||||
returning[0] = this.origin[0]
|
||||
} else {
|
||||
this.lockedDirection = 'horizontal'
|
||||
returning[1] = this.origin[1]
|
||||
if (isVertical) {
|
||||
this.lockedDirection = 'vertical'
|
||||
returning[0] = this.origin[0]
|
||||
} else {
|
||||
this.lockedDirection = 'horizontal'
|
||||
returning[1] = this.origin[1]
|
||||
}
|
||||
|
||||
this.previous = returning
|
||||
this.points.push(vec.sub(returning, this.origin))
|
||||
}
|
||||
|
||||
this.previous = returning
|
||||
this.points.push(vec.sub(returning, this.origin))
|
||||
}
|
||||
} else if (this.isLocked) {
|
||||
this.isLocked = false
|
||||
|
@ -104,19 +104,7 @@ export default class BrushSession extends BaseSession {
|
|||
// Update the points and update the shape's parents.
|
||||
const shape = getShape(data, snapshot.id) as DrawShape
|
||||
|
||||
// Offset the points and shapes to avoid negative numbers
|
||||
const ox = Math.min(newPoint[0], 0)
|
||||
const oy = Math.min(newPoint[1], 0)
|
||||
|
||||
if (ox < 0 || oy < 0) {
|
||||
const offset = [ox, oy]
|
||||
this.points = this.points.map((pt) => [...vec.sub(pt, offset), pt[2]])
|
||||
this.origin = vec.add(this.origin, offset)
|
||||
getShapeUtils(shape).translateBy(shape, offset)
|
||||
}
|
||||
|
||||
getShapeUtils(shape).setProperty(shape, 'points', [...this.points])
|
||||
|
||||
updateParents(data, [shape.id])
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,8 @@ const polygonCache = new WeakMap<DrawShape['points'], string>([])
|
|||
const draw = registerShapeUtils<DrawShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
canStyleFill: true,
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
@ -43,6 +45,11 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
}
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
// return true
|
||||
return shape.points !== prev.points || shape.style !== prev.style
|
||||
},
|
||||
|
||||
render(shape) {
|
||||
const { id, points, style } = shape
|
||||
|
||||
|
@ -171,7 +178,6 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
const styles = { ...shape.style, ...style }
|
||||
styles.dash = DashStyle.Solid
|
||||
shape.style = styles
|
||||
shape.points = [...shape.points]
|
||||
return this
|
||||
},
|
||||
|
||||
|
@ -186,8 +192,6 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
|
||||
return this
|
||||
},
|
||||
|
||||
canStyleFill: true,
|
||||
})
|
||||
|
||||
export default draw
|
||||
|
|
|
@ -108,6 +108,8 @@ const group = registerShapeUtils<GroupShape>({
|
|||
},
|
||||
|
||||
onChildrenChange(shape, children) {
|
||||
if (shape.children.length === 0) return
|
||||
|
||||
const childBounds = getCommonBounds(
|
||||
...children.map((child) => getShapeUtils(child).getRotatedBounds(child))
|
||||
)
|
||||
|
|
|
@ -155,6 +155,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
|||
this.boundsCache.delete(shape)
|
||||
return this
|
||||
},
|
||||
|
||||
shouldRender() {
|
||||
return true
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -173,6 +173,7 @@ const state = createState({
|
|||
else: ['zoomCameraToActual'],
|
||||
},
|
||||
on: {
|
||||
RESET_PAGE: 'resetPage',
|
||||
TOGGLED_READ_ONLY: 'toggleReadOnly',
|
||||
LOADED_FONTS: 'resetShapes',
|
||||
USED_PEN_DEVICE: 'enablePenLock',
|
||||
|
@ -1342,14 +1343,13 @@ const state = createState({
|
|||
},
|
||||
|
||||
// Drawing
|
||||
startDrawSession(data, payload: PointerInfo) {
|
||||
startDrawSession(data) {
|
||||
const id = Array.from(getSelectedIds(data).values())[0]
|
||||
session.begin(
|
||||
new Sessions.DrawSession(
|
||||
data,
|
||||
id,
|
||||
screenToWorld(inputs.pointer.origin, data),
|
||||
payload.shiftKey
|
||||
screenToWorld(inputs.pointer.origin, data)
|
||||
)
|
||||
)
|
||||
},
|
||||
|
@ -1501,6 +1501,9 @@ const state = createState({
|
|||
resetShapeBounds(data) {
|
||||
commands.resetBounds(data)
|
||||
},
|
||||
resetPage(data) {
|
||||
data.document.pages[data.currentPageId].shapes = {}
|
||||
},
|
||||
|
||||
/* --------------------- Editing -------------------- */
|
||||
|
||||
|
|
4
types.ts
4
types.ts
|
@ -588,5 +588,9 @@ export interface ShapeUtility<K extends Shape> {
|
|||
// Test whether bounds collide with or contain a shape.
|
||||
hitTestBounds(this: ShapeUtility<K>, shape: K, bounds: Bounds): boolean
|
||||
|
||||
// Get whether the shape should delete
|
||||
shouldDelete(this: ShapeUtility<K>, shape: K): boolean
|
||||
|
||||
// Get whether the shape should render
|
||||
shouldRender(this: ShapeUtility<K>, shape: K, previous: K): boolean
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue