Merge pull request #47 from tldraw/feature-back-to-content
Closed shape improvements, various
This commit is contained in:
commit
df2acdf884
35 changed files with 1170 additions and 3186 deletions
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
5
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -1,5 +0,0 @@
|
||||||
name: Bug Report
|
|
||||||
about: Writing and other documentation.
|
|
||||||
title: '[Bug] Bug description'
|
|
||||||
labels: bug
|
|
||||||
assignees: ''
|
|
28
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
28
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
name: Bug Report
|
||||||
|
description: File a bug report
|
||||||
|
title: '[Bug]: '
|
||||||
|
labels: [bug, triage]
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report!
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: Also tell us, what did you expect to happen?
|
||||||
|
placeholder: Tell us what you see!
|
||||||
|
value: 'A bug happened!'
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
- type: dropdown
|
||||||
|
id: browsers
|
||||||
|
attributes:
|
||||||
|
label: What browsers are you seeing the problem on?
|
||||||
|
multiple: true
|
||||||
|
options:
|
||||||
|
- Firefox
|
||||||
|
- Chrome
|
||||||
|
- Safari
|
||||||
|
- Microsoft Edge
|
|
@ -1,31 +1,142 @@
|
||||||
|
import { ShapeType } from 'types'
|
||||||
import TestState from '../test-utils'
|
import TestState from '../test-utils'
|
||||||
|
|
||||||
describe('group command', () => {
|
describe('group command', () => {
|
||||||
const tt = new TestState()
|
const tt = new TestState()
|
||||||
tt.resetDocumentState()
|
tt.resetDocumentState()
|
||||||
|
.createShape(
|
||||||
|
{
|
||||||
|
type: ShapeType.Rectangle,
|
||||||
|
point: [0, 0],
|
||||||
|
size: [100, 100],
|
||||||
|
childIndex: 1,
|
||||||
|
isLocked: false,
|
||||||
|
isHidden: false,
|
||||||
|
isAspectRatioLocked: false,
|
||||||
|
},
|
||||||
|
'rect1'
|
||||||
|
)
|
||||||
|
.createShape(
|
||||||
|
{
|
||||||
|
type: ShapeType.Rectangle,
|
||||||
|
point: [400, 0],
|
||||||
|
size: [100, 100],
|
||||||
|
childIndex: 2,
|
||||||
|
isHidden: false,
|
||||||
|
isLocked: false,
|
||||||
|
isAspectRatioLocked: false,
|
||||||
|
},
|
||||||
|
'rect2'
|
||||||
|
)
|
||||||
|
.save()
|
||||||
|
|
||||||
describe('when one item is selected', () => {
|
// it('deletes the group if it has only one child', () => {
|
||||||
it('does not change anything', () => {
|
// tt.restore()
|
||||||
// TODO
|
// .clickShape('rect1')
|
||||||
null
|
// .clickShape('rect2', { shiftKey: true })
|
||||||
})
|
// .send('GROUPED')
|
||||||
|
|
||||||
|
// const groupId = tt.getShape('rect1').parentId
|
||||||
|
|
||||||
|
// expect(groupId === tt.data.currentPageId).toBe(false)
|
||||||
|
|
||||||
|
// tt.doubleClickShape('rect1')
|
||||||
|
|
||||||
|
// tt.send('DELETED')
|
||||||
|
|
||||||
|
// expect(tt.getShape(groupId)).toBe(undefined)
|
||||||
|
// expect(tt.getShape('rect2')).toBeTruthy()
|
||||||
|
// })
|
||||||
|
|
||||||
|
it('deletes the group if all children are deleted', () => {
|
||||||
|
tt.restore()
|
||||||
|
.clickShape('rect1')
|
||||||
|
.clickShape('rect2', { shiftKey: true })
|
||||||
|
.send('GROUPED')
|
||||||
|
|
||||||
|
const groupId = tt.getShape('rect1').parentId
|
||||||
|
|
||||||
|
expect(groupId === tt.data.currentPageId).toBe(false)
|
||||||
|
|
||||||
|
tt.doubleClickShape('rect1').clickShape('rect2', { shiftKey: true })
|
||||||
|
|
||||||
|
tt.send('DELETED')
|
||||||
|
|
||||||
|
expect(tt.getShape(groupId)).toBe(undefined)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('when multiple items are selected', () => {
|
it('creates a group', () => {
|
||||||
it('does command', () => {
|
tt.restore()
|
||||||
// TODO
|
.clickShape('rect1')
|
||||||
null
|
.clickShape('rect2', { shiftKey: true })
|
||||||
})
|
.send('GROUPED')
|
||||||
|
|
||||||
it('un-does command', () => {
|
const groupId = tt.getShape('rect1').parentId
|
||||||
// TODO
|
|
||||||
null
|
|
||||||
})
|
|
||||||
|
|
||||||
it('re-does command', () => {
|
expect(groupId === tt.data.currentPageId).toBe(false)
|
||||||
// TODO
|
})
|
||||||
null
|
|
||||||
})
|
it('selects the group on single click', () => {
|
||||||
|
tt.restore()
|
||||||
|
.clickShape('rect1')
|
||||||
|
.clickShape('rect2', { shiftKey: true })
|
||||||
|
.send('GROUPED')
|
||||||
|
.clickShape('rect1')
|
||||||
|
|
||||||
|
const groupId = tt.getShape('rect1').parentId
|
||||||
|
|
||||||
|
expect(tt.selectedIds).toEqual([groupId])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selects the item on double click', () => {
|
||||||
|
tt.restore()
|
||||||
|
.clickShape('rect1')
|
||||||
|
.clickShape('rect2', { shiftKey: true })
|
||||||
|
.send('GROUPED')
|
||||||
|
.doubleClickShape('rect1')
|
||||||
|
|
||||||
|
const groupId = tt.getShape('rect1').parentId
|
||||||
|
|
||||||
|
expect(tt.data.currentParentId).toBe(groupId)
|
||||||
|
|
||||||
|
expect(tt.selectedIds).toEqual(['rect1'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resets currentPageId when clicking the canvas', () => {
|
||||||
|
tt.restore()
|
||||||
|
.clickShape('rect1')
|
||||||
|
.clickShape('rect2', { shiftKey: true })
|
||||||
|
.send('GROUPED')
|
||||||
|
.doubleClickShape('rect1')
|
||||||
|
.clickCanvas()
|
||||||
|
.clickShape('rect1')
|
||||||
|
|
||||||
|
const groupId = tt.getShape('rect1').parentId
|
||||||
|
|
||||||
|
expect(tt.data.currentParentId).toBe(tt.data.currentPageId)
|
||||||
|
|
||||||
|
expect(tt.selectedIds).toEqual([groupId])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates a group and undoes and redoes', () => {
|
||||||
|
tt.restore()
|
||||||
|
.clickShape('rect1')
|
||||||
|
.clickShape('rect2', { shiftKey: true })
|
||||||
|
.send('GROUPED')
|
||||||
|
|
||||||
|
const groupId = tt.getShape('rect1').parentId
|
||||||
|
|
||||||
|
expect(groupId === tt.data.currentPageId).toBe(false)
|
||||||
|
|
||||||
|
tt.undo()
|
||||||
|
|
||||||
|
expect(tt.getShape('rect1').parentId === tt.data.currentPageId).toBe(true)
|
||||||
|
expect(tt.getShape(groupId)).toBe(undefined)
|
||||||
|
|
||||||
|
tt.redo()
|
||||||
|
|
||||||
|
expect(tt.getShape('rect1').parentId === tt.data.currentPageId).toBe(false)
|
||||||
|
expect(tt.getShape(groupId)).toBeTruthy()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('groups shapes with different parents', () => {
|
it('groups shapes with different parents', () => {
|
||||||
|
|
|
@ -1,19 +1,61 @@
|
||||||
import state from 'state'
|
import { ArrowShape, ShapeType } from 'types'
|
||||||
import * as json from '../__mocks__/document.json'
|
import TestState from '../test-utils'
|
||||||
|
|
||||||
state.reset()
|
|
||||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
|
||||||
state.send('CLEARED_PAGE')
|
|
||||||
|
|
||||||
describe('arrow shape', () => {
|
describe('arrow shape', () => {
|
||||||
it('creates shape', () => {
|
const tt = new TestState()
|
||||||
// TODO
|
tt.resetDocumentState().save()
|
||||||
null
|
|
||||||
})
|
|
||||||
|
|
||||||
it('cancels shape while creating', () => {
|
describe('creating arrows', () => {
|
||||||
// TODO
|
it('creates shape', () => {
|
||||||
null
|
tt.reset().restore().send('SELECTED_ARROW_TOOL')
|
||||||
|
|
||||||
|
expect(tt.state.isIn('arrow.creating')).toBe(true)
|
||||||
|
|
||||||
|
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
const id = tt.getSortedPageShapeIds()[0]
|
||||||
|
|
||||||
|
const shape = tt.getShape<ArrowShape>(id)
|
||||||
|
|
||||||
|
tt.assertShapeType(id, ShapeType.Arrow)
|
||||||
|
|
||||||
|
expect(shape.handles.start.point).toEqual([0, 0])
|
||||||
|
expect(shape.handles.bend.point).toEqual([50.5, 50.5])
|
||||||
|
expect(shape.handles.end.point).toEqual([101, 101])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates shapes when pointing a shape', () => {
|
||||||
|
tt.reset().restore().send('SELECTED_ARROW_TOOL').send('TOGGLED_TOOL_LOCK')
|
||||||
|
|
||||||
|
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates shapes when shape locked', () => {
|
||||||
|
tt.reset()
|
||||||
|
.restore()
|
||||||
|
.createShape(
|
||||||
|
{
|
||||||
|
type: ShapeType.Rectangle,
|
||||||
|
point: [0, 0],
|
||||||
|
size: [100, 100],
|
||||||
|
childIndex: 1,
|
||||||
|
},
|
||||||
|
'rect1'
|
||||||
|
)
|
||||||
|
.send('SELECTED_ARROW_TOOL')
|
||||||
|
|
||||||
|
tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels shape while creating', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('moves shape', () => {
|
it('moves shape', () => {
|
||||||
|
|
101
__tests__/shapes/draw.test.ts
Normal file
101
__tests__/shapes/draw.test.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { ShapeType } from 'types'
|
||||||
|
import TestState from '../test-utils'
|
||||||
|
|
||||||
|
describe('draw shape', () => {
|
||||||
|
const tt = new TestState()
|
||||||
|
tt.resetDocumentState().save()
|
||||||
|
|
||||||
|
describe('creating draws', () => {
|
||||||
|
it('creates shape', () => {
|
||||||
|
tt.reset().restore().send('SELECTED_DRAW_TOOL')
|
||||||
|
|
||||||
|
expect(tt.state.isIn('draw.creating')).toBe(true)
|
||||||
|
|
||||||
|
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
const id = tt.getSortedPageShapeIds()[0]
|
||||||
|
|
||||||
|
tt.assertShapeType(id, ShapeType.Draw)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates shapes when pointing a shape', () => {
|
||||||
|
tt.reset().restore().send('SELECTED_DRAW_TOOL').send('TOGGLED_TOOL_LOCK')
|
||||||
|
|
||||||
|
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates shapes when shape locked', () => {
|
||||||
|
tt.reset()
|
||||||
|
.restore()
|
||||||
|
.createShape(
|
||||||
|
{
|
||||||
|
type: ShapeType.Rectangle,
|
||||||
|
point: [0, 0],
|
||||||
|
size: [100, 100],
|
||||||
|
childIndex: 1,
|
||||||
|
},
|
||||||
|
'rect1'
|
||||||
|
)
|
||||||
|
.send('SELECTED_DRAW_TOOL')
|
||||||
|
|
||||||
|
tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels shape while creating', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rotates shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rotates shape in a group', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('measures shape bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('measures shape rotated bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms single shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms in a group', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
/* -------------------- Specific -------------------- */
|
||||||
|
|
||||||
|
it('closes the shape when the start and end points are near enough', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('remains closed after resizing up', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
})
|
104
__tests__/shapes/ellipse.test.ts
Normal file
104
__tests__/shapes/ellipse.test.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import { ShapeType } from 'types'
|
||||||
|
import TestState from '../test-utils'
|
||||||
|
|
||||||
|
describe('ellipse shape', () => {
|
||||||
|
const tt = new TestState()
|
||||||
|
tt.resetDocumentState().save()
|
||||||
|
|
||||||
|
describe('creating ellipses', () => {
|
||||||
|
it('creates shape', () => {
|
||||||
|
tt.reset().restore().send('SELECTED_ELLIPSE_TOOL')
|
||||||
|
|
||||||
|
expect(tt.state.isIn('ellipse.creating')).toBe(true)
|
||||||
|
|
||||||
|
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
const id = tt.getSortedPageShapeIds()[0]
|
||||||
|
|
||||||
|
tt.assertShapeType(id, ShapeType.Ellipse)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates shapes when pointing a shape', () => {
|
||||||
|
tt.reset()
|
||||||
|
.restore()
|
||||||
|
.send('SELECTED_ELLIPSE_TOOL')
|
||||||
|
.send('TOGGLED_TOOL_LOCK')
|
||||||
|
|
||||||
|
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates shapes when shape locked', () => {
|
||||||
|
tt.reset()
|
||||||
|
.restore()
|
||||||
|
.createShape(
|
||||||
|
{
|
||||||
|
type: ShapeType.Rectangle,
|
||||||
|
point: [0, 0],
|
||||||
|
size: [100, 100],
|
||||||
|
childIndex: 1,
|
||||||
|
},
|
||||||
|
'rect1'
|
||||||
|
)
|
||||||
|
.send('SELECTED_ELLIPSE_TOOL')
|
||||||
|
|
||||||
|
tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels shape while creating', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rotates shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rotates shape in a group', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('measures shape bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('measures shape rotated bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms single shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms in a group', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
/* -------------------- Specific -------------------- */
|
||||||
|
|
||||||
|
it('creates aspect-ratio-locked shape with shift key', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resizes aspect-ratio-locked shape with shift key', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
})
|
104
__tests__/shapes/rectangle.test.ts
Normal file
104
__tests__/shapes/rectangle.test.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
import { ShapeType } from 'types'
|
||||||
|
import TestState from '../test-utils'
|
||||||
|
|
||||||
|
describe('rectangle shape', () => {
|
||||||
|
const tt = new TestState()
|
||||||
|
tt.resetDocumentState().save()
|
||||||
|
|
||||||
|
describe('creating rectangles', () => {
|
||||||
|
it('creates shape', () => {
|
||||||
|
tt.reset().restore().send('SELECTED_RECTANGLE_TOOL')
|
||||||
|
|
||||||
|
expect(tt.state.isIn('rectangle.creating')).toBe(true)
|
||||||
|
|
||||||
|
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
const id = tt.getSortedPageShapeIds()[0]
|
||||||
|
|
||||||
|
tt.assertShapeType(id, ShapeType.Rectangle)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates shapes when pointing a shape', () => {
|
||||||
|
tt.reset()
|
||||||
|
.restore()
|
||||||
|
.send('SELECTED_RECTANGLE_TOOL')
|
||||||
|
.send('TOGGLED_TOOL_LOCK')
|
||||||
|
|
||||||
|
tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('creates shapes when shape locked', () => {
|
||||||
|
tt.reset()
|
||||||
|
.restore()
|
||||||
|
.createShape(
|
||||||
|
{
|
||||||
|
type: ShapeType.Rectangle,
|
||||||
|
point: [0, 0],
|
||||||
|
size: [100, 100],
|
||||||
|
childIndex: 1,
|
||||||
|
},
|
||||||
|
'rect1'
|
||||||
|
)
|
||||||
|
.send('SELECTED_RECTANGLE_TOOL')
|
||||||
|
|
||||||
|
tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas')
|
||||||
|
|
||||||
|
expect(tt.getSortedPageShapeIds()).toHaveLength(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels shape while creating', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rotates shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rotates shape in a group', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('measures shape bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('measures shape rotated bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms single shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms in a group', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
/* -------------------- Specific -------------------- */
|
||||||
|
|
||||||
|
it('creates aspect-ratio-locked shape with shift key', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('resizes aspect-ratio-locked shape with shift key', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
})
|
79
__tests__/shapes/text.test.ts
Normal file
79
__tests__/shapes/text.test.ts
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import TestState from '../test-utils'
|
||||||
|
|
||||||
|
describe('arrow shape', () => {
|
||||||
|
const tt = new TestState()
|
||||||
|
tt.resetDocumentState()
|
||||||
|
|
||||||
|
it('creates shape', () => {
|
||||||
|
tt.send('SELECTED_TEXT_TOOL')
|
||||||
|
|
||||||
|
expect(tt.state.isIn('text.creating')).toBe(true)
|
||||||
|
|
||||||
|
const id = tt.getSortedPageShapeIds()[0]
|
||||||
|
|
||||||
|
tt.clickCanvas()
|
||||||
|
|
||||||
|
expect(tt.state.isIn('editingShape')).toBe(true)
|
||||||
|
|
||||||
|
tt.send('EDITED_SHAPE', {
|
||||||
|
id,
|
||||||
|
change: { text: 'Hello world' },
|
||||||
|
})
|
||||||
|
|
||||||
|
tt.send('BLURRED_EDITING_SHAPE', { id: id })
|
||||||
|
|
||||||
|
expect(tt.state.isIn('selecting')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cancels shape while creating', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rotates shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rotates shape in a group', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('measures shape bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('measures shape rotated bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms single shape', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms in a group', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
/* -------------------- Specific -------------------- */
|
||||||
|
|
||||||
|
it('scales', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('selects different text on tap while editing', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
})
|
|
@ -106,7 +106,7 @@ class TestState {
|
||||||
*/
|
*/
|
||||||
getSortedPageShapeIds(): string[] {
|
getSortedPageShapeIds(): string[] {
|
||||||
return Object.values(
|
return Object.values(
|
||||||
this.data.document.pages[this.data.currentParentId].shapes
|
this.data.document.pages[this.data.currentPageId].shapes
|
||||||
)
|
)
|
||||||
.sort((a, b) => a.childIndex - b.childIndex)
|
.sort((a, b) => a.childIndex - b.childIndex)
|
||||||
.map((shape) => shape.id)
|
.map((shape) => shape.id)
|
||||||
|
@ -257,6 +257,14 @@ class TestState {
|
||||||
const shape = tld.getShape(this.data, id)
|
const shape = tld.getShape(this.data, id)
|
||||||
const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0]
|
const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0]
|
||||||
|
|
||||||
|
if (id === 'canvas') {
|
||||||
|
this.state.send(
|
||||||
|
'POINTED_CANVAS',
|
||||||
|
inputs.pointerDown(TestState.point({ x, y, ...options }), id)
|
||||||
|
)
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
this.state.send(
|
this.state.send(
|
||||||
'POINTED_SHAPE',
|
'POINTED_SHAPE',
|
||||||
inputs.pointerDown(TestState.point({ x, y, ...options }), id)
|
inputs.pointerDown(TestState.point({ x, y, ...options }), id)
|
||||||
|
|
38
__tests__/tools.test.ts
Normal file
38
__tests__/tools.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import TestState from './test-utils'
|
||||||
|
|
||||||
|
const TOOLS = [
|
||||||
|
'draw',
|
||||||
|
'rectangle',
|
||||||
|
'ellipse',
|
||||||
|
'arrow',
|
||||||
|
'text',
|
||||||
|
'line',
|
||||||
|
'ray',
|
||||||
|
'dot',
|
||||||
|
]
|
||||||
|
|
||||||
|
describe('when selecting tools', () => {
|
||||||
|
const tt = new TestState()
|
||||||
|
|
||||||
|
TOOLS.forEach((tool) => {
|
||||||
|
it(`selects ${tool} tool`, () => {
|
||||||
|
tt.reset().send(`SELECTED_${tool.toUpperCase()}_TOOL`)
|
||||||
|
|
||||||
|
expect(tt.data.activeTool).toBe(tool)
|
||||||
|
expect(tt.state.isIn(tool)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
TOOLS.forEach((otherTool) => {
|
||||||
|
if (otherTool === tool) return
|
||||||
|
|
||||||
|
it(`selects ${tool} tool from ${otherTool} tool`, () => {
|
||||||
|
tt.reset()
|
||||||
|
.send(`SELECTED_${tool.toUpperCase()}_TOOL`)
|
||||||
|
.send(`SELECTED_${otherTool.toUpperCase()}_TOOL`)
|
||||||
|
|
||||||
|
expect(tt.data.activeTool).toBe(otherTool)
|
||||||
|
expect(tt.state.isIn(otherTool)).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -19,11 +19,7 @@ export default function Bounds(): JSX.Element {
|
||||||
|
|
||||||
const bounds = useSelector((s) => s.values.selectedBounds)
|
const bounds = useSelector((s) => s.values.selectedBounds)
|
||||||
|
|
||||||
const rotation = useSelector((s) =>
|
const rotation = useSelector((s) => s.values.selectedRotation)
|
||||||
s.values.selectedIds.length === 1
|
|
||||||
? tld.getSelectedShapes(s.data)[0].rotation
|
|
||||||
: 0
|
|
||||||
)
|
|
||||||
|
|
||||||
const isAllLocked = useSelector((s) => {
|
const isAllLocked = useSelector((s) => {
|
||||||
const page = tld.getPage(s.data)
|
const page = tld.getPage(s.data)
|
||||||
|
|
|
@ -33,18 +33,7 @@ export default function BoundsBg(): JSX.Element {
|
||||||
s.isInAny('selecting', 'selectPinching')
|
s.isInAny('selecting', 'selectPinching')
|
||||||
)
|
)
|
||||||
|
|
||||||
const rotation = useSelector((s) => {
|
const rotation = useSelector((s) => s.values.selectedRotation)
|
||||||
const selectedIds = s.values.selectedIds
|
|
||||||
|
|
||||||
if (selectedIds.length === 1) {
|
|
||||||
const selected = selectedIds[0]
|
|
||||||
const page = tld.getPage(s.data)
|
|
||||||
|
|
||||||
return page.shapes[selected]?.rotation
|
|
||||||
} else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const isAllHandles = useSelector((s) => {
|
const isAllHandles = useSelector((s) => {
|
||||||
const selectedIds = s.values.selectedIds
|
const selectedIds = s.values.selectedIds
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
// This is the code library.
|
|
||||||
|
|
||||||
export default `
|
|
||||||
class Circle {
|
|
||||||
greet(): string {
|
|
||||||
return "Hello!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`
|
|
|
@ -1,8 +0,0 @@
|
||||||
export default `new Circle({
|
|
||||||
point: [200, 200],
|
|
||||||
})
|
|
||||||
|
|
||||||
new Rectangle({
|
|
||||||
point: [400, 300],
|
|
||||||
})
|
|
||||||
`
|
|
|
@ -62,6 +62,8 @@ enum FontSize {
|
||||||
ExtraLarge = 'ExtraLarge',
|
ExtraLarge = 'ExtraLarge',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Theme = 'dark' | 'light'
|
||||||
|
|
||||||
type ShapeStyles = {
|
type ShapeStyles = {
|
||||||
color: ColorStyle
|
color: ColorStyle
|
||||||
size: SizeStyle
|
size: SizeStyle
|
||||||
|
@ -214,6 +216,16 @@ interface CodeResult {
|
||||||
error: CodeError
|
error: CodeError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ShapeTreeNode {
|
||||||
|
shape: Shape
|
||||||
|
children: ShapeTreeNode[]
|
||||||
|
isEditing: boolean
|
||||||
|
isHovered: boolean
|
||||||
|
isSelected: boolean
|
||||||
|
isDarkMode: boolean
|
||||||
|
isCurrentParent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
/* Editor UI */
|
/* Editor UI */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
@ -545,8 +557,12 @@ interface ShapeUtility<K extends Shape> {
|
||||||
render(
|
render(
|
||||||
this: ShapeUtility<K>,
|
this: ShapeUtility<K>,
|
||||||
shape: K,
|
shape: K,
|
||||||
info: {
|
info?: {
|
||||||
isEditing: boolean
|
isEditing?: boolean
|
||||||
|
isHovered?: boolean
|
||||||
|
isSelected?: boolean
|
||||||
|
isCurrentParent?: boolean
|
||||||
|
isDarkMode?: boolean
|
||||||
ref?: React.MutableRefObject<HTMLTextAreaElement>
|
ref?: React.MutableRefObject<HTMLTextAreaElement>
|
||||||
}
|
}
|
||||||
): JSX.Element
|
): JSX.Element
|
||||||
|
@ -636,6 +652,8 @@ enum FontSize {
|
||||||
ExtraLarge = 'ExtraLarge',
|
ExtraLarge = 'ExtraLarge',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Theme = 'dark' | 'light'
|
||||||
|
|
||||||
type ShapeStyles = {
|
type ShapeStyles = {
|
||||||
color: ColorStyle
|
color: ColorStyle
|
||||||
size: SizeStyle
|
size: SizeStyle
|
||||||
|
@ -788,6 +806,16 @@ interface CodeResult {
|
||||||
error: CodeError
|
error: CodeError
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ShapeTreeNode {
|
||||||
|
shape: Shape
|
||||||
|
children: ShapeTreeNode[]
|
||||||
|
isEditing: boolean
|
||||||
|
isHovered: boolean
|
||||||
|
isSelected: boolean
|
||||||
|
isDarkMode: boolean
|
||||||
|
isCurrentParent: boolean
|
||||||
|
}
|
||||||
|
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
/* Editor UI */
|
/* Editor UI */
|
||||||
/* -------------------------------------------------- */
|
/* -------------------------------------------------- */
|
||||||
|
@ -1119,8 +1147,12 @@ interface ShapeUtility<K extends Shape> {
|
||||||
render(
|
render(
|
||||||
this: ShapeUtility<K>,
|
this: ShapeUtility<K>,
|
||||||
shape: K,
|
shape: K,
|
||||||
info: {
|
info?: {
|
||||||
isEditing: boolean
|
isEditing?: boolean
|
||||||
|
isHovered?: boolean
|
||||||
|
isSelected?: boolean
|
||||||
|
isCurrentParent?: boolean
|
||||||
|
isDarkMode?: boolean
|
||||||
ref?: React.MutableRefObject<HTMLTextAreaElement>
|
ref?: React.MutableRefObject<HTMLTextAreaElement>
|
||||||
}
|
}
|
||||||
): JSX.Element
|
): JSX.Element
|
||||||
|
@ -1840,6 +1872,14 @@ type RequiredKeys<T> = {
|
||||||
return Array.from(new Set(items).values())
|
return Array.from(new Set(items).values())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a set to an array.
|
||||||
|
* @param set
|
||||||
|
*/
|
||||||
|
static setToArray<T>(set: Set<T>): T[] {
|
||||||
|
return Array.from(set.values())
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the outer of between a circle and a point.
|
* Get the outer of between a circle and a point.
|
||||||
* @param C The circle's center.
|
* @param C The circle's center.
|
||||||
|
|
|
@ -9,10 +9,13 @@ export default function StatusBar(): JSX.Element {
|
||||||
|
|
||||||
const shapesInView = state.values.shapesToRender.length
|
const shapesInView = state.values.shapesToRender.length
|
||||||
|
|
||||||
const active = local.active.slice(1).map((s) => {
|
const active = local.active
|
||||||
const states = s.split('.')
|
.slice(1)
|
||||||
return states[states.length - 1]
|
.map((s) => {
|
||||||
})
|
const states = s.split('.')
|
||||||
|
return states[states.length - 1]
|
||||||
|
})
|
||||||
|
.join(' | ')
|
||||||
|
|
||||||
const log = local.log[0]
|
const log = local.log[0]
|
||||||
|
|
||||||
|
@ -21,7 +24,7 @@ export default function StatusBar(): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<StatusBarContainer size={size}>
|
<StatusBarContainer size={size}>
|
||||||
<Section>
|
<Section>
|
||||||
{active.join(' | ')} - {log}
|
{active} - {log}
|
||||||
</Section>
|
</Section>
|
||||||
<Section>{shapesInView || '0'} Shapes</Section>
|
<Section>{shapesInView || '0'} Shapes</Section>
|
||||||
</StatusBarContainer>
|
</StatusBarContainer>
|
||||||
|
|
|
@ -21,7 +21,7 @@ export default function IsFilledPicker(): JSX.Element {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
selectedShapes.length === 0 ||
|
selectedShapes.length === 0 ||
|
||||||
selectedShapes.every((shape) => getShapeUtils(shape).canStyleFill)
|
selectedShapes.some((shape) => getShapeUtils(shape).canStyleFill)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -135,7 +135,7 @@ export function PrimaryButton({
|
||||||
children,
|
children,
|
||||||
}: PrimaryToolButtonProps): JSX.Element {
|
}: PrimaryToolButtonProps): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Tooltip label={label}>
|
<Tooltip label={label[0].toUpperCase() + label.slice(1)}>
|
||||||
<PrimaryToolButton
|
<PrimaryToolButton
|
||||||
name={label}
|
name={label}
|
||||||
bp={{
|
bp={{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||||
import { MutableRefObject, useCallback } from 'react'
|
import { MutableRefObject, useCallback, useEffect } from 'react'
|
||||||
import state from 'state'
|
import state from 'state'
|
||||||
import {
|
import {
|
||||||
fastBrushSelect,
|
fastBrushSelect,
|
||||||
|
@ -86,6 +86,51 @@ export default function useCanvasEvents(
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const preventGestureNavigation = (event: TouchEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
const preventNavigation = (event: TouchEvent) => {
|
||||||
|
// Center point of the touch area
|
||||||
|
const touchXPosition = event.touches[0].pageX
|
||||||
|
// Size of the touch area
|
||||||
|
const touchXRadius = event.touches[0].radiusX || 0
|
||||||
|
|
||||||
|
// We set a threshold (10px) on both sizes of the screen,
|
||||||
|
// if the touch area overlaps with the screen edges
|
||||||
|
// it's likely to trigger the navigation. We prevent the
|
||||||
|
// touchstart event in that case.
|
||||||
|
if (
|
||||||
|
touchXPosition - touchXRadius < 10 ||
|
||||||
|
touchXPosition + touchXRadius > window.innerWidth - 10
|
||||||
|
) {
|
||||||
|
event.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rCanvas.current.addEventListener('gestureend', preventGestureNavigation)
|
||||||
|
rCanvas.current.addEventListener('gesturechange', preventGestureNavigation)
|
||||||
|
rCanvas.current.addEventListener('gesturestart', preventGestureNavigation)
|
||||||
|
rCanvas.current.addEventListener('touchstart', preventNavigation)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
rCanvas.current.removeEventListener(
|
||||||
|
'gestureend',
|
||||||
|
preventGestureNavigation
|
||||||
|
)
|
||||||
|
rCanvas.current.removeEventListener(
|
||||||
|
'gesturechange',
|
||||||
|
preventGestureNavigation
|
||||||
|
)
|
||||||
|
rCanvas.current.removeEventListener(
|
||||||
|
'gesturestart',
|
||||||
|
preventGestureNavigation
|
||||||
|
)
|
||||||
|
rCanvas.current.removeEventListener('touchstart', preventNavigation)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
onPointerDown: handlePointerDown,
|
onPointerDown: handlePointerDown,
|
||||||
onPointerMove: handlePointerMove,
|
onPointerMove: handlePointerMove,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import isMobile from 'ismobilejs'
|
import { isMobile } from 'utils'
|
||||||
import { useEffect } from 'react'
|
import { useEffect } from 'react'
|
||||||
import state from 'state'
|
import state from 'state'
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ function handleFocusOut() {
|
||||||
|
|
||||||
export default function useSafariFocusOutFix(): void {
|
export default function useSafariFocusOutFix(): void {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile().apple) {
|
if (isMobile()) {
|
||||||
document.addEventListener('focusout', handleFocusOut)
|
document.addEventListener('focusout', handleFocusOut)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
testPathIgnorePatterns: ['node_modules', '.next'],
|
testPathIgnorePatterns: ['node_modules', '.next'],
|
||||||
transformIgnorePatterns: ['node_modules/(?!(sucrase|browser-fs-access)/)'],
|
transformIgnorePatterns: [
|
||||||
|
'node_modules/(?!(sucrase|@state-designer/core|@state-designer/react|browser-fs-access)/)',
|
||||||
|
],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.(ts|tsx|mjs)$': 'babel-jest',
|
'^.+\\.(ts|tsx|mjs)$': 'babel-jest',
|
||||||
},
|
},
|
||||||
|
|
|
@ -46,13 +46,14 @@
|
||||||
"@sentry/react": "^6.8.0",
|
"@sentry/react": "^6.8.0",
|
||||||
"@sentry/tracing": "^6.8.0",
|
"@sentry/tracing": "^6.8.0",
|
||||||
"@sentry/webpack-plugin": "^1.15.1",
|
"@sentry/webpack-plugin": "^1.15.1",
|
||||||
"@state-designer/react": "^1.7.4",
|
"@state-designer/react": "^2.0.3",
|
||||||
"@stitches/react": "^0.2.2",
|
"@stitches/react": "^0.2.2",
|
||||||
"@types/uuid": "^8.3.0",
|
"@types/uuid": "^8.3.0",
|
||||||
"browser-fs-access": "^0.17.3",
|
"browser-fs-access": "^0.17.3",
|
||||||
"framer-motion": "^4.1.17",
|
"framer-motion": "^4.1.17",
|
||||||
"gtag": "^1.0.1",
|
"gtag": "^1.0.1",
|
||||||
"idb-keyval": "^5.0.6",
|
"idb-keyval": "^5.0.6",
|
||||||
|
"immer": "^9.0.5",
|
||||||
"ismobilejs": "^1.1.1",
|
"ismobilejs": "^1.1.1",
|
||||||
"monaco-editor": "^0.25.2",
|
"monaco-editor": "^0.25.2",
|
||||||
"next": "^11.0.1",
|
"next": "^11.0.1",
|
||||||
|
|
|
@ -55,13 +55,10 @@ export class BaseCommand<T extends any> {
|
||||||
redo = (data: T, initial = false): void => {
|
redo = (data: T, initial = false): void => {
|
||||||
if (this.manualSelection) {
|
if (this.manualSelection) {
|
||||||
this.doFn(data, initial)
|
this.doFn(data, initial)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initial) {
|
if (!initial) {
|
||||||
this.restoreBeforeSelectionState = this.saveSelectionState(data)
|
|
||||||
} else {
|
|
||||||
this.restoreBeforeSelectionState(data)
|
this.restoreBeforeSelectionState(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
37
state/commands/create-shapes.ts
Normal file
37
state/commands/create-shapes.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import Command from './command'
|
||||||
|
import history from '../history'
|
||||||
|
import { Data, Shape } from 'types'
|
||||||
|
import tld from 'utils/tld'
|
||||||
|
import { deepClone } from 'utils'
|
||||||
|
|
||||||
|
// Used when creating new shapes.
|
||||||
|
|
||||||
|
export default function createShapesCommand(
|
||||||
|
data: Data,
|
||||||
|
shapes: Shape[],
|
||||||
|
name = 'create_shapes'
|
||||||
|
): void {
|
||||||
|
const snapshot = deepClone(shapes)
|
||||||
|
const shapeIds = snapshot.map((shape) => shape.id)
|
||||||
|
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name,
|
||||||
|
category: 'canvas',
|
||||||
|
manualSelection: true,
|
||||||
|
do(data) {
|
||||||
|
tld.createShapes(data, snapshot)
|
||||||
|
tld.setSelectedIds(data, shapeIds)
|
||||||
|
data.hoveredId = undefined
|
||||||
|
data.currentParentId = undefined
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
tld.deleteShapes(data, shapeIds)
|
||||||
|
tld.setSelectedIds(data, [])
|
||||||
|
data.hoveredId = undefined
|
||||||
|
data.currentParentId = undefined
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,111 +0,0 @@
|
||||||
import Command from './command'
|
|
||||||
import history from '../history'
|
|
||||||
import { Data, Shape } from 'types'
|
|
||||||
import { deepClone } from 'utils'
|
|
||||||
import tld from 'utils/tld'
|
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
|
||||||
|
|
||||||
export default function deleteSelected(data: Data): void {
|
|
||||||
const selectedShapes = tld.getSelectedShapes(data)
|
|
||||||
|
|
||||||
const selectedIdsArr = selectedShapes
|
|
||||||
.filter((shape) => !shape.isLocked)
|
|
||||||
.map((shape) => shape.id)
|
|
||||||
|
|
||||||
const shapeIdsToDelete = selectedIdsArr.flatMap((id) =>
|
|
||||||
tld.getDocumentBranch(data, id)
|
|
||||||
)
|
|
||||||
|
|
||||||
const remainingIds = selectedShapes
|
|
||||||
.filter((shape) => shape.isLocked)
|
|
||||||
.map((shape) => shape.id)
|
|
||||||
|
|
||||||
let deletedShapes: Shape[] = []
|
|
||||||
|
|
||||||
history.execute(
|
|
||||||
data,
|
|
||||||
new Command({
|
|
||||||
name: 'delete_selection',
|
|
||||||
category: 'canvas',
|
|
||||||
manualSelection: true,
|
|
||||||
do(data) {
|
|
||||||
// Update selected ids
|
|
||||||
tld.setSelectedIds(data, remainingIds)
|
|
||||||
|
|
||||||
// Recursively delete shapes (and maybe their parents too)
|
|
||||||
deletedShapes = deleteShapes(data, shapeIdsToDelete)
|
|
||||||
},
|
|
||||||
undo(data) {
|
|
||||||
const page = tld.getPage(data)
|
|
||||||
|
|
||||||
// Update selected ids
|
|
||||||
tld.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 = tld.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
|
|
||||||
}
|
|
37
state/commands/delete-shapes.ts
Normal file
37
state/commands/delete-shapes.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import Command from './command'
|
||||||
|
import history from '../history'
|
||||||
|
import { Data, Shape } from 'types'
|
||||||
|
import tld from 'utils/tld'
|
||||||
|
|
||||||
|
export default function deleteShapes(data: Data, shapes: Shape[]): void {
|
||||||
|
const initialSelectedIds = [...tld.getSelectedIds(data)]
|
||||||
|
|
||||||
|
const shapeIdsToDelete = shapes.flatMap((shape) =>
|
||||||
|
shape.isLocked ? [] : tld.getDocumentBranch(data, shape.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
const remainingIds = initialSelectedIds.filter(
|
||||||
|
(id) => !shapeIdsToDelete.includes(id)
|
||||||
|
)
|
||||||
|
|
||||||
|
// We're going to delete the shapes and their children, too; and possibly
|
||||||
|
// their parents, if we delete all of a group shape's children.
|
||||||
|
let deletedShapes: Shape[] = []
|
||||||
|
|
||||||
|
history.execute(
|
||||||
|
data,
|
||||||
|
new Command({
|
||||||
|
name: 'delete_selection',
|
||||||
|
category: 'canvas',
|
||||||
|
manualSelection: true,
|
||||||
|
do(data) {
|
||||||
|
deletedShapes = tld.deleteShapes(data, shapeIdsToDelete)
|
||||||
|
tld.setSelectedIds(data, remainingIds)
|
||||||
|
},
|
||||||
|
undo(data) {
|
||||||
|
tld.createShapes(data, deletedShapes)
|
||||||
|
tld.setSelectedIds(data, initialSelectedIds)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,7 +2,8 @@ import align from './align'
|
||||||
import changePage from './change-page'
|
import changePage from './change-page'
|
||||||
import createPage from './create-page'
|
import createPage from './create-page'
|
||||||
import deletePage from './delete-page'
|
import deletePage from './delete-page'
|
||||||
import deleteSelected from './delete-selected'
|
import deleteShapes from './delete-shapes'
|
||||||
|
import createShapes from './create-shapes'
|
||||||
import distribute from './distribute'
|
import distribute from './distribute'
|
||||||
import doublePointHandle from './double-point-handle'
|
import doublePointHandle from './double-point-handle'
|
||||||
import draw from './draw'
|
import draw from './draw'
|
||||||
|
@ -30,8 +31,9 @@ const commands = {
|
||||||
align,
|
align,
|
||||||
changePage,
|
changePage,
|
||||||
createPage,
|
createPage,
|
||||||
|
createShapes,
|
||||||
deletePage,
|
deletePage,
|
||||||
deleteSelected,
|
deleteShapes,
|
||||||
distribute,
|
distribute,
|
||||||
doublePointHandle,
|
doublePointHandle,
|
||||||
draw,
|
draw,
|
||||||
|
|
|
@ -125,11 +125,13 @@ export default class DrawSession extends BaseSession {
|
||||||
const page = tld.getPage(data)
|
const page = tld.getPage(data)
|
||||||
const shape = page.shapes[snapshot.id] as DrawShape
|
const shape = page.shapes[snapshot.id] as DrawShape
|
||||||
|
|
||||||
if (shape.points.length < this.points.length) {
|
if (vec.dist(this.points[0], this.points[this.points.length - 1]) < 8) {
|
||||||
getShapeUtils(shape).setProperty(shape, 'points', this.points)
|
this.points.push(this.points[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
getShapeUtils(shape).onSessionComplete(shape)
|
getShapeUtils(shape)
|
||||||
|
.setProperty(shape, 'points', this.points)
|
||||||
|
.onSessionComplete(shape)
|
||||||
|
|
||||||
tld.updateParents(data, [shape.id])
|
tld.updateParents(data, [shape.id])
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,20 @@ export default class HandleSession extends BaseSession {
|
||||||
shiftKey: boolean
|
shiftKey: boolean
|
||||||
initialShape: Shape
|
initialShape: Shape
|
||||||
handleId: string
|
handleId: string
|
||||||
|
isCreating: boolean
|
||||||
|
|
||||||
constructor(data: Data, shapeId: string, handleId: string, point: number[]) {
|
constructor(
|
||||||
|
data: Data,
|
||||||
|
shapeId: string,
|
||||||
|
handleId: string,
|
||||||
|
point: number[],
|
||||||
|
isCreating: boolean
|
||||||
|
) {
|
||||||
super(data)
|
super(data)
|
||||||
this.origin = point
|
this.origin = point
|
||||||
this.handleId = handleId
|
this.handleId = handleId
|
||||||
this.initialShape = deepClone(tld.getShape(data, shapeId))
|
this.initialShape = deepClone(tld.getShape(data, shapeId))
|
||||||
|
this.isCreating = isCreating
|
||||||
}
|
}
|
||||||
|
|
||||||
update(
|
update(
|
||||||
|
@ -48,13 +56,21 @@ export default class HandleSession extends BaseSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(data: Data): void {
|
cancel(data: Data): void {
|
||||||
tld.getPage(data).shapes[this.initialShape.id] = this.initialShape
|
if (this.isCreating) {
|
||||||
|
tld.deleteShapes(data, [this.initialShape])
|
||||||
|
} else {
|
||||||
|
tld.getPage(data).shapes[this.initialShape.id] = this.initialShape
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
complete(data: Data): void {
|
complete(data: Data): void {
|
||||||
const before = this.initialShape
|
const before = this.initialShape
|
||||||
const after = deepClone(tld.getShape(data, before.id))
|
const after = deepClone(tld.getShape(data, before.id))
|
||||||
commands.mutate(data, [before], [after])
|
if (this.isCreating) {
|
||||||
|
commands.createShapes(data, [after])
|
||||||
|
} else {
|
||||||
|
commands.mutate(data, [before], [after])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -59,17 +59,15 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{style.isFilled && (
|
<ellipse
|
||||||
<ellipse
|
cx={radiusX}
|
||||||
cx={radiusX}
|
cy={radiusY}
|
||||||
cy={radiusY}
|
rx={rx}
|
||||||
rx={rx}
|
ry={ry}
|
||||||
ry={ry}
|
stroke="none"
|
||||||
stroke="none"
|
fill={style.isFilled ? styles.fill : 'transparent'}
|
||||||
fill={styles.fill}
|
pointerEvents="all"
|
||||||
pointerEvents="fill"
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<path
|
<path
|
||||||
d={path}
|
d={path}
|
||||||
fill={styles.stroke}
|
fill={styles.stroke}
|
||||||
|
@ -106,7 +104,7 @@ const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
strokeWidth={sw}
|
strokeWidth={sw}
|
||||||
strokeDasharray={strokeDasharray}
|
strokeDasharray={strokeDasharray}
|
||||||
strokeDashoffset={strokeDashoffset}
|
strokeDashoffset={strokeDashoffset}
|
||||||
pointerEvents={style.isFilled ? 'all' : 'stroke'}
|
pointerEvents="all"
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -98,6 +98,10 @@ const group = registerShapeUtils<GroupShape>({
|
||||||
return this
|
return this
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shouldDelete(shape) {
|
||||||
|
return shape.children.length === 0 // should be <= 1
|
||||||
|
},
|
||||||
|
|
||||||
onChildrenChange(shape, children) {
|
onChildrenChange(shape, children) {
|
||||||
if (shape.children.length === 0) return
|
if (shape.children.length === 0) return
|
||||||
|
|
||||||
|
|
|
@ -40,26 +40,23 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{style.isFilled && (
|
<rect
|
||||||
<rect
|
rx={radius}
|
||||||
rx={radius}
|
ry={radius}
|
||||||
ry={radius}
|
x={+styles.strokeWidth / 2}
|
||||||
x={+styles.strokeWidth / 2}
|
y={+styles.strokeWidth / 2}
|
||||||
y={+styles.strokeWidth / 2}
|
width={Math.max(0, size[0] - strokeWidth)}
|
||||||
width={Math.max(0, size[0] - strokeWidth)}
|
height={Math.max(0, size[1] - strokeWidth)}
|
||||||
height={Math.max(0, size[1] - strokeWidth)}
|
fill={style.isFilled ? styles.fill : 'transparent'}
|
||||||
strokeWidth={0}
|
stroke="none"
|
||||||
fill={styles.fill}
|
/>
|
||||||
stroke={styles.stroke}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<path
|
<path
|
||||||
d={pathData}
|
d={pathData}
|
||||||
fill={styles.stroke}
|
fill={styles.stroke}
|
||||||
stroke={styles.stroke}
|
stroke={styles.stroke}
|
||||||
strokeWidth={styles.strokeWidth}
|
strokeWidth={styles.strokeWidth}
|
||||||
filter={isHovered ? 'url(#expand)' : 'none'}
|
filter={isHovered ? 'url(#expand)' : 'none'}
|
||||||
pointerEvents={style.isFilled ? 'all' : 'stroke'}
|
pointerEvents="all"
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -110,7 +107,7 @@ const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
fill={styles.fill}
|
fill={styles.fill}
|
||||||
stroke="transparent"
|
stroke="transparent"
|
||||||
strokeWidth={sw}
|
strokeWidth={sw}
|
||||||
pointerEvents={style.isFilled ? 'all' : 'stroke'}
|
pointerEvents="all"
|
||||||
/>
|
/>
|
||||||
<g filter={isHovered ? 'url(#expand)' : 'none'} pointerEvents="stroke">
|
<g filter={isHovered ? 'url(#expand)' : 'none'} pointerEvents="stroke">
|
||||||
{paths}
|
{paths}
|
||||||
|
|
|
@ -93,8 +93,8 @@ const initialData: Data = {
|
||||||
const draw = new Draw({
|
const draw = new Draw({
|
||||||
points: [
|
points: [
|
||||||
...Utils.getPointsBetween([0, 0], [20, 50]),
|
...Utils.getPointsBetween([0, 0], [20, 50]),
|
||||||
...Utils.getPointsBetween([20, 50], [100, 20], 3),
|
...Utils.getPointsBetween([20, 50], [100, 20], { steps: 3 }),
|
||||||
...Utils.getPointsBetween([100, 20], [100, 100], 10),
|
...Utils.getPointsBetween([100, 20], [100, 100], { steps: 10 }),
|
||||||
[100, 100],
|
[100, 100],
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
@ -937,6 +937,9 @@ const state = createState({
|
||||||
POINTED_CANVAS: {
|
POINTED_CANVAS: {
|
||||||
to: 'ellipse.editing',
|
to: 'ellipse.editing',
|
||||||
},
|
},
|
||||||
|
POINTED_SHAPE: {
|
||||||
|
to: 'ellipse.editing',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
editing: {
|
editing: {
|
||||||
|
@ -1453,7 +1456,7 @@ const state = createState({
|
||||||
breakSession(data) {
|
breakSession(data) {
|
||||||
session.cancel(data)
|
session.cancel(data)
|
||||||
history.disable()
|
history.disable()
|
||||||
commands.deleteSelected(data)
|
commands.deleteShapes(data, tld.getSelectedShapes(data))
|
||||||
history.enable()
|
history.enable()
|
||||||
},
|
},
|
||||||
cancelSession(data) {
|
cancelSession(data) {
|
||||||
|
@ -1550,7 +1553,8 @@ const state = createState({
|
||||||
data,
|
data,
|
||||||
shapeId,
|
shapeId,
|
||||||
handleId,
|
handleId,
|
||||||
tld.screenToWorld(inputs.pointer.origin, data)
|
tld.screenToWorld(inputs.pointer.origin, data),
|
||||||
|
false
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -1667,7 +1671,8 @@ const state = createState({
|
||||||
data,
|
data,
|
||||||
shapeId,
|
shapeId,
|
||||||
handleId,
|
handleId,
|
||||||
tld.screenToWorld(inputs.pointer.origin, data)
|
tld.screenToWorld(inputs.pointer.origin, data),
|
||||||
|
true
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -1778,7 +1783,7 @@ const state = createState({
|
||||||
commands.toggle(data, 'isAspectRatioLocked')
|
commands.toggle(data, 'isAspectRatioLocked')
|
||||||
},
|
},
|
||||||
deleteSelection(data) {
|
deleteSelection(data) {
|
||||||
commands.deleteSelected(data)
|
commands.deleteShapes(data, tld.getSelectedShapes(data))
|
||||||
},
|
},
|
||||||
rotateSelectionCcw(data) {
|
rotateSelectionCcw(data) {
|
||||||
commands.rotateCcw(data)
|
commands.rotateCcw(data)
|
||||||
|
@ -2250,7 +2255,18 @@ const state = createState({
|
||||||
|
|
||||||
return commonStyle
|
return commonStyle
|
||||||
},
|
},
|
||||||
|
selectedRotation(data) {
|
||||||
|
const selectedIds = tld.getSelectedIds(data)
|
||||||
|
|
||||||
|
if (selectedIds.length === 1) {
|
||||||
|
const selected = selectedIds[0]
|
||||||
|
const page = tld.getPage(data)
|
||||||
|
|
||||||
|
return page.shapes[selected]?.rotation
|
||||||
|
} else {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
},
|
||||||
shapesToRender(data) {
|
shapesToRender(data) {
|
||||||
const viewport = tld.getViewport(data)
|
const viewport = tld.getViewport(data)
|
||||||
|
|
||||||
|
|
150
utils/tld.ts
150
utils/tld.ts
|
@ -15,6 +15,7 @@ import {
|
||||||
ShapeTreeNode,
|
ShapeTreeNode,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import { AssertionError } from 'assert'
|
import { AssertionError } from 'assert'
|
||||||
|
import { lerp } from './utils'
|
||||||
|
|
||||||
export default class StateUtils {
|
export default class StateUtils {
|
||||||
static getCameraZoom(zoom: number): number {
|
static getCameraZoom(zoom: number): number {
|
||||||
|
@ -93,6 +94,155 @@ export default class StateUtils {
|
||||||
return Object.values(page.shapes)
|
return Object.values(page.shapes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the shapes to the current page.
|
||||||
|
*
|
||||||
|
* ### Example
|
||||||
|
*
|
||||||
|
*```ts
|
||||||
|
* tld.createShape(data, [shape1])
|
||||||
|
* tld.createShape(data, [shape1, shape2, shape3])
|
||||||
|
*```
|
||||||
|
*/
|
||||||
|
static createShapes(data: Data, shapes: Shape[]): void {
|
||||||
|
const page = this.getPage(data)
|
||||||
|
const shapeIds = shapes.map((shape) => shape.id)
|
||||||
|
|
||||||
|
// Update selected ids
|
||||||
|
this.setSelectedIds(data, shapeIds)
|
||||||
|
|
||||||
|
// Restore deleted shapes
|
||||||
|
shapes.forEach((shape) => {
|
||||||
|
const newShape = { ...shape }
|
||||||
|
page.shapes[shape.id] = newShape
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update parents
|
||||||
|
shapes.forEach((shape) => {
|
||||||
|
if (shape.parentId === data.currentPageId) return
|
||||||
|
|
||||||
|
const parent = page.shapes[shape.parentId]
|
||||||
|
|
||||||
|
getShapeUtils(parent)
|
||||||
|
.setProperty(
|
||||||
|
parent,
|
||||||
|
'children',
|
||||||
|
parent.children.includes(shape.id)
|
||||||
|
? parent.children
|
||||||
|
: [...parent.children, shape.id]
|
||||||
|
)
|
||||||
|
.onChildrenChange(
|
||||||
|
parent,
|
||||||
|
parent.children.map((id) => page.shapes[id])
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the shapes from the current page.
|
||||||
|
*
|
||||||
|
* ### Example
|
||||||
|
*
|
||||||
|
*```ts
|
||||||
|
* tld.deleteShape(data, [shape1])
|
||||||
|
* tld.deleteShape(data, [shape1, shape1, shape1])
|
||||||
|
*```
|
||||||
|
*/
|
||||||
|
static deleteShapes(
|
||||||
|
data: Data,
|
||||||
|
shapeIds: string[] | Shape[],
|
||||||
|
shapesDeleted: Shape[] = []
|
||||||
|
): Shape[] {
|
||||||
|
const ids =
|
||||||
|
typeof shapeIds[0] === 'string'
|
||||||
|
? (shapeIds as string[])
|
||||||
|
: (shapeIds as Shape[]).map((shape) => shape.id)
|
||||||
|
|
||||||
|
const parentsToDelete: string[] = []
|
||||||
|
|
||||||
|
const page = this.getPage(data)
|
||||||
|
|
||||||
|
const parentIds = new Set(ids.map((id) => page.shapes[id].parentId))
|
||||||
|
|
||||||
|
// Delete shapes
|
||||||
|
ids.forEach((id) => {
|
||||||
|
shapesDeleted.push(deepClone(page.shapes[id]))
|
||||||
|
delete page.shapes[id]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update parents
|
||||||
|
parentIds.forEach((id) => {
|
||||||
|
const parent = page.shapes[id]
|
||||||
|
|
||||||
|
// The parent was either deleted or a is a page.
|
||||||
|
if (!parent) return
|
||||||
|
|
||||||
|
const utils = getShapeUtils(parent)
|
||||||
|
|
||||||
|
// Remove deleted ids from the parent's children and update the parent
|
||||||
|
utils
|
||||||
|
.setProperty(
|
||||||
|
parent,
|
||||||
|
'children',
|
||||||
|
parent.children.filter((childId) => !ids.includes(childId))
|
||||||
|
)
|
||||||
|
.onChildrenChange(
|
||||||
|
parent,
|
||||||
|
parent.children.map((id) => page.shapes[id])
|
||||||
|
)
|
||||||
|
|
||||||
|
if (utils.shouldDelete(parent)) {
|
||||||
|
// If the parent decides it should delete, then we need to reparent
|
||||||
|
// the parent's remaining children to the parent's parent, and
|
||||||
|
// assign them correct child indices, and then delete the parent on
|
||||||
|
// the next recursive step.
|
||||||
|
|
||||||
|
const nextIndex = this.getChildIndexAbove(data, parent.id)
|
||||||
|
|
||||||
|
const len = parent.children.length
|
||||||
|
|
||||||
|
// Reparent the children and assign them new child indices
|
||||||
|
parent.children.forEach((childId, i) => {
|
||||||
|
const child = this.getShape(data, childId)
|
||||||
|
|
||||||
|
getShapeUtils(child)
|
||||||
|
.setProperty(child, 'parentId', parent.parentId)
|
||||||
|
.setProperty(
|
||||||
|
child,
|
||||||
|
'childIndex',
|
||||||
|
lerp(parent.childIndex, nextIndex, i / len)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (parent.parentId !== page.id) {
|
||||||
|
// If the parent is not a page, then we add the parent's children
|
||||||
|
// to the parent's parent shape before emptying that array. If the
|
||||||
|
// parent is a page, then we don't need to do this step.
|
||||||
|
// TODO: Consider adding explicit children array to page shapes.
|
||||||
|
const grandParent = page.shapes[parent.parentId]
|
||||||
|
|
||||||
|
getShapeUtils(grandParent)
|
||||||
|
.setProperty(grandParent, 'children', [...parent.children])
|
||||||
|
.onChildrenChange(
|
||||||
|
grandParent,
|
||||||
|
grandParent.children.map((id) => page.shapes[id])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty the parent's children array and delete the parent on the next
|
||||||
|
// iteration step.
|
||||||
|
getShapeUtils(parent).setProperty(parent, 'children', [])
|
||||||
|
parentsToDelete.push(parent.id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (parentsToDelete.length > 0) {
|
||||||
|
return this.deleteShapes(data, parentsToDelete, shapesDeleted)
|
||||||
|
}
|
||||||
|
|
||||||
|
return shapesDeleted
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current selected shapes as an array.
|
* Get the current selected shapes as an array.
|
||||||
* @param data
|
* @param data
|
||||||
|
|
Loading…
Reference in a new issue