Adds more tests, simplifies draw tool

This commit is contained in:
Steve Ruiz 2021-06-26 12:52:36 +01:00
parent 75e60d5eb2
commit ff58073d12
17 changed files with 521 additions and 20437 deletions

File diff suppressed because it is too large Load diff

View 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

View file

@ -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)
// // })
// })

View file

@ -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)
})

View file

@ -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
}

View file

@ -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

View file

@ -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
View 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
}
)
}

View file

@ -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
}
}
}

View file

@ -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
}

View file

@ -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])
}

View file

@ -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

View file

@ -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))
)

View file

@ -155,6 +155,10 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
this.boundsCache.delete(shape)
return this
},
shouldRender() {
return true
},
}
}

View file

@ -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 -------------------- */

View file

@ -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
}