Fix bug on missing others, adds new tests
This commit is contained in:
parent
50d4517d0d
commit
8ee78d1b90
59 changed files with 2745 additions and 1450 deletions
34
.vscode/snippets.code-snippets
vendored
Normal file
34
.vscode/snippets.code-snippets
vendored
Normal file
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
// Place your tldraw workspace snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
|
||||
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
|
||||
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
|
||||
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
|
||||
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
|
||||
// Placeholders with the same ids are connected.
|
||||
// Example:
|
||||
// "Print to console": {
|
||||
// "scope": "javascript,typescript",
|
||||
// "prefix": "log",
|
||||
// "body": [
|
||||
// "console.log('$1');",
|
||||
// "$2"
|
||||
// ],
|
||||
// "description": "Log output to console"
|
||||
// }
|
||||
"createComment": {
|
||||
"scope": "typescript",
|
||||
"prefix": "/**",
|
||||
"body": [
|
||||
"/**",
|
||||
" * ${1:description}",
|
||||
" *",
|
||||
" * ### Example",
|
||||
" *",
|
||||
" *```ts",
|
||||
" * ${2:example}",
|
||||
" *```",
|
||||
" */"
|
||||
],
|
||||
"description": "comment"
|
||||
}
|
||||
}
|
|
@ -116,3 +116,120 @@ Object {
|
|||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`restoring project remounts the state after mutating the current state: data after re-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",
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
|
72
__tests__/bounds.test.ts
Normal file
72
__tests__/bounds.test.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import { getCommonBounds } from 'utils'
|
||||
import TestState, { arrowId, rectangleId } from './test-utils'
|
||||
|
||||
describe('selection', () => {
|
||||
const tt = new TestState()
|
||||
|
||||
it('measures correct bounds for selected item', () => {
|
||||
// Note: Each item should test its own bounds in its ./shapes/[shape].tsx file
|
||||
|
||||
const shape = tt.getShape(rectangleId)
|
||||
|
||||
tt.deselectAll().clickShape(rectangleId)
|
||||
|
||||
expect(tt.state.values.selectedBounds).toStrictEqual(
|
||||
getShapeUtils(shape).getBounds(shape)
|
||||
)
|
||||
})
|
||||
|
||||
it('measures correct bounds for rotated selected item', () => {
|
||||
const shape = tt.getShape(rectangleId)
|
||||
|
||||
getShapeUtils(shape).rotateBy(shape, Math.PI * 2 * Math.random())
|
||||
|
||||
tt.deselectAll().clickShape(rectangleId)
|
||||
|
||||
expect(tt.state.values.selectedBounds).toStrictEqual(
|
||||
getShapeUtils(shape).getBounds(shape)
|
||||
)
|
||||
|
||||
getShapeUtils(shape).rotateBy(shape, -Math.PI * 2 * Math.random())
|
||||
|
||||
expect(tt.state.values.selectedBounds).toStrictEqual(
|
||||
getShapeUtils(shape).getBounds(shape)
|
||||
)
|
||||
})
|
||||
|
||||
it('measures correct bounds for selected items', () => {
|
||||
const shape1 = tt.getShape(rectangleId)
|
||||
const shape2 = tt.getShape(arrowId)
|
||||
|
||||
tt.deselectAll()
|
||||
.clickShape(shape1.id)
|
||||
.clickShape(shape2.id, { shiftKey: true })
|
||||
|
||||
expect(tt.state.values.selectedBounds).toStrictEqual(
|
||||
getCommonBounds(
|
||||
getShapeUtils(shape1).getRotatedBounds(shape1),
|
||||
getShapeUtils(shape2).getRotatedBounds(shape2)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('measures correct bounds for rotated selected items', () => {
|
||||
const shape1 = tt.getShape(rectangleId)
|
||||
const shape2 = tt.getShape(arrowId)
|
||||
|
||||
getShapeUtils(shape1).rotateBy(shape1, Math.PI * 2 * Math.random())
|
||||
getShapeUtils(shape2).rotateBy(shape2, Math.PI * 2 * Math.random())
|
||||
|
||||
tt.deselectAll()
|
||||
.clickShape(shape1.id)
|
||||
.clickShape(shape2.id, { shiftKey: true })
|
||||
|
||||
expect(tt.state.values.selectedBounds).toStrictEqual(
|
||||
getCommonBounds(
|
||||
getShapeUtils(shape1).getRotatedBounds(shape1),
|
||||
getShapeUtils(shape2).getRotatedBounds(shape2)
|
||||
)
|
||||
)
|
||||
})
|
||||
})
|
300
__tests__/children.test.ts
Normal file
300
__tests__/children.test.ts
Normal file
|
@ -0,0 +1,300 @@
|
|||
import { MoveType, ShapeType } from 'types'
|
||||
import TestState from './test-utils'
|
||||
|
||||
describe('shapes with children', () => {
|
||||
const tt = new TestState()
|
||||
|
||||
tt.resetDocumentState()
|
||||
.createShape(
|
||||
{
|
||||
type: ShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
childIndex: 1,
|
||||
},
|
||||
'delete-me-bottom'
|
||||
)
|
||||
.createShape(
|
||||
{
|
||||
type: ShapeType.Rectangle,
|
||||
point: [0, 0],
|
||||
size: [100, 100],
|
||||
childIndex: 2,
|
||||
},
|
||||
'1'
|
||||
)
|
||||
.createShape(
|
||||
{
|
||||
type: ShapeType.Rectangle,
|
||||
point: [300, 0],
|
||||
size: [100, 100],
|
||||
childIndex: 3,
|
||||
},
|
||||
'2'
|
||||
)
|
||||
.createShape(
|
||||
{
|
||||
type: ShapeType.Rectangle,
|
||||
point: [0, 300],
|
||||
size: [100, 100],
|
||||
childIndex: 4,
|
||||
},
|
||||
'delete-me-middle'
|
||||
)
|
||||
.createShape(
|
||||
{
|
||||
type: ShapeType.Rectangle,
|
||||
point: [0, 300],
|
||||
size: [100, 100],
|
||||
childIndex: 5,
|
||||
},
|
||||
'3'
|
||||
)
|
||||
.createShape(
|
||||
{
|
||||
type: ShapeType.Rectangle,
|
||||
point: [300, 300],
|
||||
size: [100, 100],
|
||||
childIndex: 6,
|
||||
},
|
||||
'4'
|
||||
)
|
||||
|
||||
// Delete shapes at the start and in the middle of the list
|
||||
|
||||
tt.clickShape('delete-me-bottom')
|
||||
.send('DELETED')
|
||||
.clickShape('delete-me-middle')
|
||||
.send('DELETED')
|
||||
|
||||
it('has shapes in order', () => {
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.childIndex)
|
||||
).toStrictEqual([2, 3, 5, 6])
|
||||
})
|
||||
|
||||
it('moves a shape to back', () => {
|
||||
tt.clickShape('3').send('MOVED', {
|
||||
type: MoveType.ToBack,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '1', '2', '4'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings to back', () => {
|
||||
tt.clickShape('4').clickShape('2', { shiftKey: true }).send('MOVED', {
|
||||
type: MoveType.ToBack,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '4', '3', '1'])
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings to back', () => {
|
||||
tt.clickShape('4').clickShape('1', { shiftKey: true }).send('MOVED', {
|
||||
type: MoveType.ToBack,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '1', '2', '3'])
|
||||
})
|
||||
|
||||
it('moves a shape backward', () => {
|
||||
tt.clickShape('3').send('MOVED', {
|
||||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '1', '3', '2'])
|
||||
})
|
||||
|
||||
it('moves a shape at first index backward', () => {
|
||||
tt.clickShape('4').send('MOVED', {
|
||||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '1', '3', '2'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings backward', () => {
|
||||
tt.clickShape('3').clickShape('2', { shiftKey: true }).send('MOVED', {
|
||||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '3', '2', '1'])
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings backward', () => {
|
||||
tt.clickShape('3').clickShape('1', { shiftKey: true }).send('MOVED', {
|
||||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '4', '1', '2'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings backward at zero index', () => {
|
||||
tt.clickShape('3').clickShape('4', { shiftKey: true }).send('MOVED', {
|
||||
type: MoveType.Backward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '4', '1', '2'])
|
||||
})
|
||||
|
||||
it('moves a shape forward', () => {
|
||||
tt.clickShape('4').send('MOVED', {
|
||||
type: MoveType.Forward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '1', '4', '2'])
|
||||
})
|
||||
|
||||
it('moves a shape forward at the top index', () => {
|
||||
tt.clickShape('2').send('MOVED', {
|
||||
type: MoveType.Forward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '1', '4', '2'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings forward', () => {
|
||||
tt.deselectAll()
|
||||
.clickShape('4')
|
||||
.clickShape('1', { shiftKey: true })
|
||||
.send('MOVED', {
|
||||
type: MoveType.Forward,
|
||||
})
|
||||
|
||||
expect(tt.idsAreSelected(['1', '4'])).toBe(true)
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['3', '2', '1', '4'])
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings forward', () => {
|
||||
tt.deselectAll()
|
||||
.clickShape('3')
|
||||
.clickShape('1', { shiftKey: true })
|
||||
.send('MOVED', {
|
||||
type: MoveType.Forward,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '3', '4', '1'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings forward at top index', () => {
|
||||
tt.deselectAll()
|
||||
.clickShape('3')
|
||||
.clickShape('1', { shiftKey: true })
|
||||
.send('MOVED', {
|
||||
type: MoveType.Forward,
|
||||
})
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '4', '3', '1'])
|
||||
})
|
||||
|
||||
it('moves a shape to front', () => {
|
||||
tt.deselectAll().clickShape('2').send('MOVED', {
|
||||
type: MoveType.ToFront,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '3', '1', '2'])
|
||||
})
|
||||
|
||||
it('moves two adjacent siblings to front', () => {
|
||||
tt.deselectAll()
|
||||
.clickShape('3')
|
||||
.clickShape('1', { shiftKey: true })
|
||||
.send('MOVED', {
|
||||
type: MoveType.ToFront,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['4', '2', '3', '1'])
|
||||
})
|
||||
|
||||
it('moves two non-adjacent siblings to front', () => {
|
||||
tt.deselectAll()
|
||||
.clickShape('4')
|
||||
.clickShape('3', { shiftKey: true })
|
||||
.send('MOVED', {
|
||||
type: MoveType.ToFront,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '1', '4', '3'])
|
||||
})
|
||||
|
||||
it('moves siblings already at front to front', () => {
|
||||
tt.deselectAll()
|
||||
.clickShape('4')
|
||||
.clickShape('3', { shiftKey: true })
|
||||
.send('MOVED', {
|
||||
type: MoveType.ToFront,
|
||||
})
|
||||
|
||||
expect(
|
||||
Object.values(tt.data.document.pages[tt.data.currentParentId].shapes)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.map((shape) => shape.id)
|
||||
).toStrictEqual(['2', '1', '4', '3'])
|
||||
})
|
||||
})
|
46
__tests__/coop.test.ts
Normal file
46
__tests__/coop.test.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import state from 'state'
|
||||
import coopState from 'state/coop/coop-state'
|
||||
import * as json from './__mocks__/document.json'
|
||||
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
state.send('CLEARED_PAGE')
|
||||
|
||||
coopState.reset()
|
||||
|
||||
describe('coop', () => {
|
||||
it('joins a room', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('leaves a room', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('rejoins a room', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('handles another user joining room', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('handles another user leaving room', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('sends mouse movements', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('receives mouse movements', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
})
|
|
@ -7,18 +7,22 @@ state.send('CLEARED_PAGE')
|
|||
|
||||
describe('arrow shape', () => {
|
||||
it('creates a shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('cancels shape while creating', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('removes shape on undo and restores it on redo', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not create shape when readonly', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { getPerfectDashProps } from 'utils/dashes'
|
||||
import { getPerfectDashProps } from 'utils'
|
||||
|
||||
describe('ellipse dash props', () => {
|
||||
it('renders dashed props on a circle correctly', () => {
|
||||
|
|
|
@ -1,157 +1,120 @@
|
|||
import state from 'state'
|
||||
import inputs from 'state/inputs'
|
||||
import { ShapeType } from 'types'
|
||||
import {
|
||||
idsAreSelected,
|
||||
point,
|
||||
rectangleId,
|
||||
arrowId,
|
||||
getOnlySelectedShape,
|
||||
assertShapeProps,
|
||||
} from './test-utils'
|
||||
import tld from 'utils/tld'
|
||||
import * as json from './__mocks__/document.json'
|
||||
import TestState, { rectangleId, arrowId } from './test-utils'
|
||||
|
||||
describe('deleting single shapes', () => {
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
const tt = new TestState()
|
||||
|
||||
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))
|
||||
describe('deleting single shapes', () => {
|
||||
it('deletes a shape and undoes the delete', () => {
|
||||
tt.deselectAll().clickShape(rectangleId).pressDelete()
|
||||
|
||||
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
expect(tt.idsAreSelected([])).toBe(true)
|
||||
|
||||
state.send('DELETED')
|
||||
expect(tt.getShape(rectangleId)).toBe(undefined)
|
||||
|
||||
expect(idsAreSelected(state.data, [])).toBe(true)
|
||||
expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
|
||||
tt.undo()
|
||||
|
||||
state.send('UNDO')
|
||||
expect(tt.getShape(rectangleId)).toBeTruthy()
|
||||
expect(tt.idsAreSelected([rectangleId])).toBe(true)
|
||||
|
||||
expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
|
||||
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
tt.redo()
|
||||
|
||||
state.send('REDO')
|
||||
|
||||
expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
|
||||
|
||||
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', () => {
|
||||
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 = tld.getShape(state.data, arrowId)
|
||||
|
||||
// The arrow should be have the group as its parent
|
||||
expect(assertShapeProps(arrow, { parentId: group.id }))
|
||||
expect(tt.getShape(rectangleId)).toBe(undefined)
|
||||
})
|
||||
})
|
||||
|
||||
// it('selects the new group', () => {
|
||||
// expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
||||
// })
|
||||
describe('deleting and restoring grouped shapes', () => {
|
||||
it('creates a group', () => {
|
||||
tt.reset()
|
||||
.deselectAll()
|
||||
.clickShape(rectangleId)
|
||||
.clickShape(arrowId, { shiftKey: true })
|
||||
.send('GROUPED')
|
||||
|
||||
// it('assigns a new parent', () => {
|
||||
// expect(groupId === state.data.currentPageId).toBe(false)
|
||||
// })
|
||||
const group = tt.getOnlySelectedShape()
|
||||
|
||||
// // Rectangle has the same new parent?
|
||||
// it('assigns new parent to all selected shapes', () => {
|
||||
// expect(hasParent(state.data, arrowId, groupId)).toBe(true)
|
||||
// })
|
||||
// Should select the group
|
||||
expect(tt.assertShapeProps(group, { type: ShapeType.Group })).toBe(true)
|
||||
|
||||
// // New parent is selected?
|
||||
// it('selects the new parent', () => {
|
||||
// expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
||||
// })
|
||||
const arrow = tt.getShape(arrowId)
|
||||
|
||||
// The arrow should be have the group as its parent
|
||||
expect(tt.assertShapeProps(arrow, { parentId: group.id })).toBe(true)
|
||||
})
|
||||
|
||||
it('selects the new group', () => {
|
||||
const groupId = tt.getShape(arrowId).parentId
|
||||
|
||||
expect(tt.idsAreSelected([groupId])).toBe(true)
|
||||
})
|
||||
|
||||
it('assigns a new parent', () => {
|
||||
const groupId = tt.getShape(arrowId).parentId
|
||||
|
||||
expect(groupId === tt.data.currentPageId).toBe(false)
|
||||
})
|
||||
|
||||
// Rectangle has the same new parent?
|
||||
it('assigns new parent to all selected shapes', () => {
|
||||
const groupId = tt.getShape(arrowId).parentId
|
||||
|
||||
expect(tt.hasParent(arrowId, groupId)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('selecting within the group', () => {
|
||||
it('selects the group when pointing a shape', () => {
|
||||
const groupId = tt.getShape(arrowId).parentId
|
||||
|
||||
tt.deselectAll().clickShape(rectangleId)
|
||||
|
||||
expect(tt.idsAreSelected([groupId])).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps selection when pointing group shape', () => {
|
||||
const groupId = tt.getShape(arrowId).parentId
|
||||
|
||||
tt.deselectAll().clickShape(groupId)
|
||||
|
||||
expect(tt.idsAreSelected([groupId])).toBe(true)
|
||||
})
|
||||
|
||||
it('selects a grouped shape by double-pointing', () => {
|
||||
tt.deselectAll().doubleClickShape(rectangleId)
|
||||
|
||||
expect(tt.idsAreSelected([rectangleId])).toBe(true)
|
||||
})
|
||||
|
||||
it('selects a sibling on point after double-pointing into a grouped shape children', () => {
|
||||
tt.deselectAll().doubleClickShape(rectangleId).clickShape(arrowId)
|
||||
|
||||
expect(tt.idsAreSelected([arrowId])).toBe(true)
|
||||
})
|
||||
|
||||
it('rises up a selection level when escape is pressed', () => {
|
||||
const groupId = tt.getShape(arrowId).parentId
|
||||
|
||||
tt.deselectAll().doubleClickShape(rectangleId).send('CANCELLED')
|
||||
|
||||
tt.clickShape(rectangleId)
|
||||
|
||||
expect(tt.idsAreSelected([groupId])).toBe(true)
|
||||
})
|
||||
|
||||
// it('deletes and restores one shape', () => {
|
||||
// // Delete the rectangle first
|
||||
// state.send('UNDO')
|
||||
|
||||
// expect(tld.getShape(tt.data, rectangleId)).toBeTruthy()
|
||||
// expect(tt.idsAreSelected([rectangleId])).toBe(true)
|
||||
|
||||
// state.send('REDO')
|
||||
|
||||
// expect(tld.getShape(tt.data, rectangleId)).toBe(undefined)
|
||||
|
||||
// state.send('UNDO')
|
||||
|
||||
// expect(tld.getShape(tt.data, rectangleId)).toBeTruthy()
|
||||
// expect(tt.idsAreSelected([rectangleId])).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(tld.getShape(state.data, rectangleId)).toBeTruthy()
|
||||
// // expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
|
||||
// // state.send('REDO')
|
||||
|
||||
// // expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
|
||||
|
||||
// // state.send('UNDO')
|
||||
|
||||
// // expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
|
||||
// // expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
// // })
|
||||
// })
|
||||
|
|
61
__tests__/locked.test.ts
Normal file
61
__tests__/locked.test.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import TestState from './test-utils'
|
||||
|
||||
describe('locked shapes', () => {
|
||||
const tt = new TestState()
|
||||
tt.resetDocumentState()
|
||||
|
||||
it('toggles a locked shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('selects a locked shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not translate a locked shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not translate a locked shape in a group', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not rotate a locked shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not rotate a locked shape in a group', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('dpes not transform a locked single shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not transform a locked shape in a multiple selection', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not transform a locked shape in a group', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not change the style of a locked shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('does not change the handles of a locked shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
})
|
|
@ -7,12 +7,36 @@ describe('project', () => {
|
|||
|
||||
it('mounts the state', () => {
|
||||
state.send('MOUNTED')
|
||||
|
||||
expect(state.isIn('ready')).toBe(true)
|
||||
})
|
||||
|
||||
it('loads file from json', () => {
|
||||
state.send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
|
||||
expect(state.isIn('ready')).toBe(true)
|
||||
expect(state.data.document).toMatchSnapshot('data after mount from file')
|
||||
})
|
||||
})
|
||||
|
||||
describe('restoring project', () => {
|
||||
state.reset()
|
||||
state.enableLog(true)
|
||||
|
||||
it('remounts the state after mutating the current state', () => {
|
||||
state
|
||||
.send('MOUNTED')
|
||||
.send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
.send('CLEARED_PAGE')
|
||||
|
||||
expect(
|
||||
state.data.document.pages[state.data.currentPageId].shapes
|
||||
).toStrictEqual({})
|
||||
|
||||
state
|
||||
.send('MOUNTED')
|
||||
.send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
|
||||
expect(state.data.document).toMatchSnapshot('data after re-mount from file')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,139 +1,81 @@
|
|||
import state from 'state'
|
||||
import inputs from 'state/inputs'
|
||||
import { idsAreSelected, point, rectangleId, arrowId } from './test-utils'
|
||||
import * as json from './__mocks__/document.json'
|
||||
|
||||
// Mount the state and load the test file from json
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
import TestState, { rectangleId, arrowId } from './test-utils'
|
||||
|
||||
describe('selection', () => {
|
||||
it('selects a shape', () => {
|
||||
state
|
||||
.send('CANCELED')
|
||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
||||
const tt = new TestState()
|
||||
|
||||
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
it('selects a shape', () => {
|
||||
tt.deselectAll().clickShape(rectangleId)
|
||||
|
||||
expect(tt.idsAreSelected([rectangleId])).toBe(true)
|
||||
})
|
||||
|
||||
it('selects and deselects a shape', () => {
|
||||
state
|
||||
.send('CANCELED')
|
||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
||||
tt.deselectAll().clickShape(rectangleId).clickCanvas()
|
||||
|
||||
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
||||
|
||||
state
|
||||
.send('POINTED_CANVAS', inputs.pointerDown(point(), 'canvas'))
|
||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), 'canvas'))
|
||||
|
||||
expect(idsAreSelected(state.data, [])).toBe(true)
|
||||
expect(tt.idsAreSelected([])).toBe(true)
|
||||
})
|
||||
|
||||
it('selects multiple shapes', () => {
|
||||
expect(idsAreSelected(state.data, [])).toBe(true)
|
||||
tt.deselectAll()
|
||||
.clickShape(rectangleId)
|
||||
.clickShape(arrowId, { shiftKey: true })
|
||||
|
||||
state
|
||||
.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)
|
||||
expect(tt.idsAreSelected([rectangleId, arrowId])).toBe(true)
|
||||
})
|
||||
|
||||
it('shift-selects to deselect shapes', () => {
|
||||
state
|
||||
.send('CANCELLED')
|
||||
.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)
|
||||
)
|
||||
.send(
|
||||
'POINTED_SHAPE',
|
||||
inputs.pointerDown(point({ shiftKey: true }), rectangleId)
|
||||
)
|
||||
.send(
|
||||
'STOPPED_POINTING',
|
||||
inputs.pointerUp(point({ shiftKey: true }), rectangleId)
|
||||
)
|
||||
tt.deselectAll()
|
||||
.clickShape(rectangleId)
|
||||
.clickShape(arrowId, { shiftKey: true })
|
||||
.clickShape(rectangleId, { shiftKey: true })
|
||||
|
||||
expect(idsAreSelected(state.data, [arrowId])).toBe(true)
|
||||
expect(tt.idsAreSelected([arrowId])).toBe(true)
|
||||
})
|
||||
|
||||
it('single-selects shape in selection on pointerup', () => {
|
||||
state
|
||||
.send('CANCELLED')
|
||||
.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)
|
||||
)
|
||||
it('single-selects shape in selection on click', () => {
|
||||
tt.deselectAll()
|
||||
.clickShape(rectangleId)
|
||||
.clickShape(arrowId, { shiftKey: true })
|
||||
.clickShape(arrowId)
|
||||
|
||||
expect(idsAreSelected(state.data, [rectangleId, arrowId])).toBe(true)
|
||||
expect(tt.idsAreSelected([arrowId])).toBe(true)
|
||||
})
|
||||
|
||||
state.send('POINTED_SHAPE', inputs.pointerDown(point(), arrowId))
|
||||
it('single-selects shape in selection on pointerup only', () => {
|
||||
tt.deselectAll()
|
||||
.clickShape(rectangleId)
|
||||
.clickShape(arrowId, { shiftKey: true })
|
||||
|
||||
expect(idsAreSelected(state.data, [rectangleId, arrowId])).toBe(true)
|
||||
expect(tt.idsAreSelected([rectangleId, arrowId])).toBe(true)
|
||||
|
||||
state.send('STOPPED_POINTING', inputs.pointerUp(point(), arrowId))
|
||||
tt.startClick(arrowId)
|
||||
|
||||
expect(idsAreSelected(state.data, [arrowId])).toBe(true)
|
||||
expect(tt.idsAreSelected([rectangleId, arrowId])).toBe(true)
|
||||
|
||||
tt.stopClick(arrowId)
|
||||
|
||||
expect(tt.idsAreSelected([arrowId])).toBe(true)
|
||||
})
|
||||
|
||||
it('selects shapes if shift key is lifted before pointerup', () => {
|
||||
state
|
||||
.send('CANCELLED')
|
||||
.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)
|
||||
)
|
||||
.send(
|
||||
'POINTED_SHAPE',
|
||||
inputs.pointerDown(point({ shiftKey: true }), arrowId)
|
||||
)
|
||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), arrowId))
|
||||
tt.deselectAll()
|
||||
.clickShape(rectangleId)
|
||||
.clickShape(arrowId, { shiftKey: true })
|
||||
.startClick(rectangleId, { shiftKey: true })
|
||||
.stopClick(rectangleId)
|
||||
|
||||
expect(idsAreSelected(state.data, [arrowId])).toBe(true)
|
||||
expect(tt.idsAreSelected([rectangleId])).toBe(true)
|
||||
})
|
||||
|
||||
it('does not select on meta-click', () => {
|
||||
state
|
||||
.send('CANCELLED')
|
||||
.send(
|
||||
'POINTED_SHAPE',
|
||||
inputs.pointerDown(point({ ctrlKey: true }), rectangleId)
|
||||
)
|
||||
.send(
|
||||
'STOPPED_POINTING',
|
||||
inputs.pointerUp(point({ ctrlKey: true }), rectangleId)
|
||||
)
|
||||
tt.deselectAll().clickShape(rectangleId, { ctrlKey: true })
|
||||
|
||||
expect(idsAreSelected(state.data, [])).toBe(true)
|
||||
expect(tt.idsAreSelected([])).toBe(true)
|
||||
})
|
||||
|
||||
it('does not select on meta-shift-click', () => {
|
||||
tt.deselectAll().clickShape(rectangleId, { ctrlKey: true, shiftKey: true })
|
||||
|
||||
expect(tt.idsAreSelected([])).toBe(true)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -7,62 +7,74 @@ state.send('CLEARED_PAGE')
|
|||
|
||||
describe('arrow shape', () => {
|
||||
it('creates shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('cancels shape while creating', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('moves shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('rotates shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('measures bounds', () => {
|
||||
it('rotates shape in a group', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('measures rotated bounds', () => {
|
||||
it('measures shape bounds', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms single', () => {
|
||||
it('measures shape rotated bounds', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms single shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms in a group', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
/* -------------------- Specific -------------------- */
|
||||
|
||||
it('creates compass-aligned shape with shift key', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('changes start handle', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('changes end handle', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('changes bend handle', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('resets bend handle when double-pointed', () => {
|
||||
null
|
||||
})
|
||||
|
||||
/* -------------------- Readonly -------------------- */
|
||||
|
||||
it('does not create shape when readonly', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
})
|
||||
|
|
28
__tests__/style.test.ts
Normal file
28
__tests__/style.test.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import state from 'state'
|
||||
import * as json from './__mocks__/document.json'
|
||||
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
state.send('CLEARED_PAGE')
|
||||
|
||||
describe('shape styles', () => {
|
||||
it('sets the color style of a shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('sets the size style of a shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('sets the dash style of a shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('sets the isFilled style of a shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
})
|
|
@ -1,11 +1,18 @@
|
|||
import { Data, Shape, ShapeType } from 'types'
|
||||
import _state from 'state'
|
||||
import tld from 'utils/tld'
|
||||
import inputs from 'state/inputs'
|
||||
import { createShape, getShapeUtils } from 'state/shape-utils'
|
||||
import { Data, Shape, ShapeType, ShapeUtility } from 'types'
|
||||
import { deepCompareArrays, uniqueId, vec } from 'utils'
|
||||
import * as json from './__mocks__/document.json'
|
||||
|
||||
type State = typeof _state
|
||||
|
||||
export const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
|
||||
export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
|
||||
|
||||
interface PointerOptions {
|
||||
id?: string
|
||||
id?: number
|
||||
x?: number
|
||||
y?: number
|
||||
shiftKey?: boolean
|
||||
|
@ -13,77 +20,573 @@ interface PointerOptions {
|
|||
ctrlKey?: boolean
|
||||
}
|
||||
|
||||
export function point(
|
||||
options: PointerOptions = {} as PointerOptions
|
||||
): PointerEvent {
|
||||
const {
|
||||
id = '1',
|
||||
x = 0,
|
||||
y = 0,
|
||||
shiftKey = false,
|
||||
altKey = false,
|
||||
ctrlKey = false,
|
||||
} = options
|
||||
class TestState {
|
||||
state: State
|
||||
|
||||
return {
|
||||
shiftKey,
|
||||
altKey,
|
||||
ctrlKey,
|
||||
pointerId: id,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
} as any
|
||||
}
|
||||
constructor() {
|
||||
this.state = _state
|
||||
this.reset()
|
||||
}
|
||||
|
||||
export function idsAreSelected(
|
||||
data: Data,
|
||||
ids: string[],
|
||||
strict = true
|
||||
): boolean {
|
||||
const selectedIds = tld.getSelectedIds(data)
|
||||
return (
|
||||
(strict ? selectedIds.size === ids.length : true) &&
|
||||
ids.every((id) => selectedIds.has(id))
|
||||
)
|
||||
}
|
||||
/**
|
||||
* Reset the test state.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.reset()
|
||||
*```
|
||||
*/
|
||||
reset(): TestState {
|
||||
this.state.reset()
|
||||
this.state
|
||||
.send('MOUNTED')
|
||||
.send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
|
||||
export function hasParent(
|
||||
data: Data,
|
||||
childId: string,
|
||||
parentId: string
|
||||
): boolean {
|
||||
return tld.getShape(data, childId).parentId === parentId
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
export function getOnlySelectedShape(data: Data): Shape {
|
||||
const selectedShapes = tld.getSelectedShapes(data)
|
||||
return selectedShapes.length === 1 ? selectedShapes[0] : undefined
|
||||
}
|
||||
/**
|
||||
* Reset the document state. Will remove all shapes and extra pages.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.resetDocumentState()
|
||||
*```
|
||||
*/
|
||||
resetDocumentState(): TestState {
|
||||
this.state.send('RESET_DOCUMENT_STATE')
|
||||
return this
|
||||
}
|
||||
|
||||
export function assertShapeType(
|
||||
data: Data,
|
||||
shapeId: string,
|
||||
type: ShapeType
|
||||
): boolean {
|
||||
const shape = tld.getShape(data, shapeId)
|
||||
if (shape.type !== type) {
|
||||
throw new TypeError(
|
||||
`expected shape ${shapeId} to be of type ${type}, found ${shape?.type} instead`
|
||||
/**
|
||||
* Send a message to the state.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.send("MOVED_TO_FRONT")
|
||||
*```
|
||||
*/
|
||||
send(eventName: string, payload?: unknown): TestState {
|
||||
this.state.send(eventName, payload)
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new shape on the current page. Optionally provide an id.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.createShape({ type: ShapeType.Rectangle, point: [100, 100]})
|
||||
* tt.createShape({ type: ShapeType.Rectangle, point: [100, 100]}, "myId")
|
||||
*```
|
||||
*/
|
||||
createShape(props: Partial<Shape>, id = uniqueId()): TestState {
|
||||
const shape = createShape(props.type, props)
|
||||
getShapeUtils(shape).setProperty(shape, 'id', id)
|
||||
this.data.document.pages[this.data.currentPageId].shapes[shape.id] = shape
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get whether the provided ids are the current selected ids. If the `strict` argument is `true`, then the result will be false if the state has selected ids in addition to those provided.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.idsAreSelected(state.data, ['rectangleId', 'ellipseId'])
|
||||
* tt.idsAreSelected(state.data, ['rectangleId', 'ellipseId'], true)
|
||||
*```
|
||||
*/
|
||||
idsAreSelected(ids: string[], strict = true): boolean {
|
||||
const selectedIds = tld.getSelectedIds(this.data)
|
||||
return (
|
||||
(strict ? selectedIds.size === ids.length : true) &&
|
||||
ids.every((id) => selectedIds.has(id))
|
||||
)
|
||||
}
|
||||
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]) {
|
||||
/**
|
||||
* Get whether the shape with the provided id has the provided parent id.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.hasParent('childId', 'parentId')
|
||||
*```
|
||||
*/
|
||||
hasParent(childId: string, parentId: string): boolean {
|
||||
return tld.getShape(this.data, childId).parentId === parentId
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the only selected shape. If more than one shape is selected, the test will fail.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.getOnlySelectedShape()
|
||||
*```
|
||||
*/
|
||||
getOnlySelectedShape(): Shape {
|
||||
const selectedShapes = tld.getSelectedShapes(this.data)
|
||||
return selectedShapes.length === 1 ? selectedShapes[0] : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a shape has the provided type.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.example
|
||||
*```
|
||||
*/
|
||||
assertShapeType(shapeId: string, type: ShapeType): boolean {
|
||||
const shape = tld.getShape(this.data, shapeId)
|
||||
if (shape.type !== type) {
|
||||
throw new TypeError(
|
||||
`expected shape ${shape.id} to have property ${key}: ${props[key]}, found ${key}: ${shape[key]} instead`
|
||||
`expected shape ${shapeId} to be of type ${type}, found ${shape?.type} instead`
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the provided shape has the provided props.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```
|
||||
* tt.assertShapeProps(myShape, { point: [0,0], style: { color: ColorStyle.Blue } } )
|
||||
*```
|
||||
*/
|
||||
assertShapeProps<T extends Shape>(
|
||||
shape: T,
|
||||
props: { [K in keyof Partial<T>]: T[K] }
|
||||
): boolean {
|
||||
for (const key in props) {
|
||||
let result: boolean
|
||||
const value = props[key]
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
result = deepCompareArrays(value, shape[key] as typeof value)
|
||||
} else if (typeof value === 'object') {
|
||||
const target = shape[key] as typeof value
|
||||
result =
|
||||
target &&
|
||||
Object.entries(value).every(([k, v]) => target[k] === props[key][v])
|
||||
} else {
|
||||
result = shape[key] === value
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
throw new TypeError(
|
||||
`expected shape ${shape.id} to have property ${key}: ${props[key]}, found ${key}: ${shape[key]} instead`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Click a shape.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.clickShape("myShapeId")
|
||||
*```
|
||||
*/
|
||||
clickShape(id: string, options: PointerOptions = {}): TestState {
|
||||
this.state
|
||||
.send('POINTED_SHAPE', inputs.pointerDown(TestState.point(options), id))
|
||||
.send('STOPPED_POINTING', inputs.pointerUp(TestState.point(options), id))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a click (but do not stop it).
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.startClick("myShapeId")
|
||||
*```
|
||||
*/
|
||||
startClick(id: string, options: PointerOptions = {}): TestState {
|
||||
this.state.send(
|
||||
'POINTED_SHAPE',
|
||||
inputs.pointerDown(TestState.point(options), id)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a click (after starting it).
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.stopClick("myShapeId")
|
||||
*```
|
||||
*/
|
||||
stopClick(id: string, options: PointerOptions = {}): TestState {
|
||||
this.state.send(
|
||||
'STOPPED_POINTING',
|
||||
inputs.pointerUp(TestState.point(options), id)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Double click a shape.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.clickShape("myShapeId")
|
||||
*```
|
||||
*/
|
||||
doubleClickShape(id: string, options: PointerOptions = {}): TestState {
|
||||
this.state
|
||||
.send(
|
||||
'DOUBLE_POINTED_SHAPE',
|
||||
inputs.pointerDown(TestState.point(options), id)
|
||||
)
|
||||
.send('STOPPED_POINTING', inputs.pointerUp(TestState.point(options), id))
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the canvas.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.clickCanvas("myShapeId")
|
||||
*```
|
||||
*/
|
||||
clickCanvas(options: PointerOptions = {}): TestState {
|
||||
this.state
|
||||
.send(
|
||||
'POINTED_CANVAS',
|
||||
inputs.pointerDown(TestState.point(options), 'canvas')
|
||||
)
|
||||
.send(
|
||||
'STOPPED_POINTING',
|
||||
inputs.pointerUp(TestState.point(options), 'canvas')
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Click the background / body of the bounding box.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.clickBounds()
|
||||
*```
|
||||
*/
|
||||
clickBounds(options: PointerOptions = {}): TestState {
|
||||
this.state
|
||||
.send(
|
||||
'POINTED_BOUNDS',
|
||||
inputs.pointerDown(TestState.point(options), 'bounds')
|
||||
)
|
||||
.send(
|
||||
'STOPPED_POINTING',
|
||||
inputs.pointerUp(TestState.point(options), 'bounds')
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the pointer to a new point, or to several points in order.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.movePointerTo([100, 100])
|
||||
* tt.movePointerTo([100, 100], { shiftKey: true })
|
||||
* tt.movePointerTo([[100, 100], [150, 150], [200, 200]])
|
||||
*```
|
||||
*/
|
||||
movePointerTo(
|
||||
to: number[] | number[][],
|
||||
options: Omit<PointerOptions, 'x' | 'y'> = {}
|
||||
): TestState {
|
||||
if (Array.isArray(to[0])) {
|
||||
;(to as number[][]).forEach(([x, y]) => {
|
||||
this.state.send(
|
||||
'MOVED_POINTER',
|
||||
inputs.pointerMove(TestState.point({ x, y, ...options }))
|
||||
)
|
||||
})
|
||||
} else {
|
||||
const [x, y] = to as number[]
|
||||
this.state.send(
|
||||
'MOVED_POINTER',
|
||||
inputs.pointerMove(TestState.point({ x, y, ...options }))
|
||||
)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the pointer by a delta.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.movePointerBy([10,10])
|
||||
* tt.movePointerBy([10,10], { shiftKey: true })
|
||||
*```
|
||||
*/
|
||||
movePointerBy(
|
||||
by: number[] | number[][],
|
||||
options: Omit<PointerOptions, 'x' | 'y'> = {}
|
||||
): TestState {
|
||||
let pt = inputs.pointer?.point || [0, 0]
|
||||
|
||||
if (Array.isArray(by[0])) {
|
||||
;(by as number[][]).forEach((delta) => {
|
||||
pt = vec.add(pt, delta)
|
||||
|
||||
this.state.send(
|
||||
'MOVED_POINTER',
|
||||
inputs.pointerMove(
|
||||
TestState.point({ x: pt[0], y: pt[1], ...options })
|
||||
)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
pt = vec.add(pt, by as number[])
|
||||
|
||||
this.state.send(
|
||||
'MOVED_POINTER',
|
||||
inputs.pointerMove(TestState.point({ x: pt[0], y: pt[1], ...options }))
|
||||
)
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Move pointer over a shape. Will move the pointer to the top-left corner of the shape.
|
||||
*
|
||||
* ###
|
||||
* ```
|
||||
* tt.movePointerOverShape('myShapeId', [100, 100])
|
||||
* ```
|
||||
*/
|
||||
movePointerOverShape(
|
||||
id: string,
|
||||
options: Omit<PointerOptions, 'x' | 'y'> = {}
|
||||
): TestState {
|
||||
const shape = tld.getShape(this.state.data, id)
|
||||
const [x, y] = vec.add(shape.point, [1, 1])
|
||||
|
||||
this.state.send(
|
||||
'MOVED_OVER_SHAPE',
|
||||
inputs.pointerEnter(TestState.point({ x, y, ...options }), id)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the pointer over a group. Will move the pointer to the top-left corner of the group.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.movePointerOverHandle('myGroupId')
|
||||
* tt.movePointerOverHandle('myGroupId', { shiftKey: true })
|
||||
*```
|
||||
*/
|
||||
movePointerOverGroup(
|
||||
id: string,
|
||||
options: Omit<PointerOptions, 'x' | 'y'> = {}
|
||||
): TestState {
|
||||
const shape = tld.getShape(this.state.data, id)
|
||||
const [x, y] = vec.add(shape.point, [1, 1])
|
||||
|
||||
this.state.send(
|
||||
'MOVED_OVER_GROUP',
|
||||
inputs.pointerEnter(TestState.point({ x, y, ...options }), id)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the pointer over a handle. Will move the pointer to the top-left corner of the handle.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.movePointerOverHandle('bend')
|
||||
* tt.movePointerOverHandle('bend', { shiftKey: true })
|
||||
*```
|
||||
*/
|
||||
movePointerOverHandle(
|
||||
id: string,
|
||||
options: Omit<PointerOptions, 'x' | 'y'> = {}
|
||||
): TestState {
|
||||
const shape = tld.getShape(this.state.data, id)
|
||||
const handle = shape.handles?.[id]
|
||||
const [x, y] = vec.add(handle.point, [1, 1])
|
||||
|
||||
this.state.send(
|
||||
'MOVED_OVER_HANDLE',
|
||||
inputs.pointerEnter(TestState.point({ x, y, ...options }), id)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselect all shapes.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.deselectAll()
|
||||
*```
|
||||
*/
|
||||
deselectAll(): TestState {
|
||||
this.state.send('DESELECTED_ALL')
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the selected shapes
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.pressDelete()
|
||||
*```
|
||||
*/
|
||||
pressDelete(): TestState {
|
||||
this.state.send('DELETED')
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a shape and test it.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.testShape("myShapeId", myShape => myShape )
|
||||
*```
|
||||
*/
|
||||
testShape<T extends Shape>(
|
||||
id: string,
|
||||
fn: (shape: T, shapeUtils: ShapeUtility<T>) => boolean
|
||||
): boolean {
|
||||
const shape = this.getShape<T>(id)
|
||||
return fn(shape, shape && getShapeUtils(shape))
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a shape
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.getShape("myShapeId")
|
||||
*```
|
||||
*/
|
||||
getShape<T extends Shape>(id: string): T {
|
||||
return tld.getShape(this.data, id) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.undo()
|
||||
*```
|
||||
*/
|
||||
undo(): TestState {
|
||||
this.state.send('UNDO')
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Redo.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.redo()
|
||||
*```
|
||||
*/
|
||||
redo(): TestState {
|
||||
this.state.send('REDO')
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state's current data.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.data
|
||||
*```
|
||||
*/
|
||||
get data(): Readonly<Data> {
|
||||
return this.state.data
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a fake PointerEvent.
|
||||
*
|
||||
* ### Example
|
||||
*
|
||||
*```ts
|
||||
* tt.point()
|
||||
* tt.point({ x: 0, y: 0})
|
||||
* tt.point({ x: 0, y: 0, shiftKey: true } )
|
||||
*```
|
||||
*/
|
||||
static point(options: PointerOptions = {} as PointerOptions): PointerEvent {
|
||||
const {
|
||||
id = 1,
|
||||
x = 0,
|
||||
y = 0,
|
||||
shiftKey = false,
|
||||
altKey = false,
|
||||
ctrlKey = false,
|
||||
} = options
|
||||
|
||||
return {
|
||||
shiftKey,
|
||||
altKey,
|
||||
ctrlKey,
|
||||
pointerId: id,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
} as any
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export default TestState
|
||||
|
|
91
__tests__/transform.test.ts
Normal file
91
__tests__/transform.test.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import state from 'state'
|
||||
import * as json from './__mocks__/document.json'
|
||||
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
state.send('CLEARED_PAGE')
|
||||
|
||||
describe('transforms shapes', () => {
|
||||
it('transforms from the top edge', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the right edge', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the bottom edge', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the left edge', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the top-left corner', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the top-right corner', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the bottom-right corner', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the bottom-left corner', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
})
|
||||
|
||||
describe('transforms shapes while aspect-ratio locked', () => {
|
||||
// Fixed
|
||||
|
||||
it('transforms from the top edge while aspect-ratio locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the right edge while aspect-ratio locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the bottom edge while aspect-ratio locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
it('transforms from the left edge while aspect-ratio locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the top-left corner while aspect-ratio locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the top-right corner while aspect-ratio locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the bottom-right corner while aspect-ratio locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('transforms from the bottom-left corner while aspect-ratio locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
})
|
38
__tests__/translate.test.ts
Normal file
38
__tests__/translate.test.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import state from 'state'
|
||||
import * as json from './__mocks__/document.json'
|
||||
|
||||
state.reset()
|
||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||
state.send('CLEARED_PAGE')
|
||||
|
||||
describe('translates shapes', () => {
|
||||
it('translates a single selected shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('translates multiple selected shape', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('translates while axis-locked', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('translates after leaving axis-locked state', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('creates clones while translating', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
|
||||
it('removes clones after leaving cloning state', () => {
|
||||
// TODO
|
||||
null
|
||||
})
|
||||
})
|
|
@ -71,8 +71,8 @@ const MainSVG = styled('svg', {
|
|||
|
||||
function ErrorFallback({ error, resetErrorBoundary }) {
|
||||
React.useEffect(() => {
|
||||
console.error(error)
|
||||
const copy = 'Sorry, something went wrong. Clear canvas and continue?'
|
||||
console.error(error)
|
||||
if (window.confirm(copy)) {
|
||||
state.send('CLEARED_PAGE')
|
||||
resetErrorBoundary()
|
||||
|
|
|
@ -6,23 +6,24 @@ export default function Presence(): JSX.Element {
|
|||
const others = useCoopSelector((s) => s.data.others)
|
||||
const currentPageId = useSelector((s) => s.data.currentPageId)
|
||||
|
||||
if (!others) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.values(others).map(({ connectionId, presence }) => {
|
||||
if (presence === null) return null
|
||||
if (presence.pageId !== currentPageId) return null
|
||||
|
||||
return (
|
||||
<Cursor
|
||||
key={`cursor-${connectionId}`}
|
||||
color={'red'}
|
||||
duration={presence.duration}
|
||||
times={presence.times}
|
||||
bufferedXs={presence.bufferedXs}
|
||||
bufferedYs={presence.bufferedYs}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
{Object.values(others)
|
||||
.filter(({ presence }) => presence?.pageId === currentPageId)
|
||||
.map(({ connectionId, presence }) => {
|
||||
return (
|
||||
<Cursor
|
||||
key={`cursor-${connectionId}`}
|
||||
color={'red'}
|
||||
duration={presence.duration}
|
||||
times={presence.times}
|
||||
bufferedXs={presence.bufferedXs}
|
||||
bufferedYs={presence.bufferedYs}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,11 +8,11 @@ import useShapeEvents from 'hooks/useShapeEvents'
|
|||
import vec from 'utils/vec'
|
||||
import { getShapeStyle } from 'state/shape-styles'
|
||||
import useShapeDef from 'hooks/useShape'
|
||||
import { ShapeUtility } from 'types'
|
||||
import { BooleanArraySupportOption } from 'prettier'
|
||||
|
||||
interface ShapeProps {
|
||||
id: string
|
||||
isSelecting: boolean
|
||||
isSelecting: BooleanArraySupportOption
|
||||
}
|
||||
|
||||
function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
|
||||
|
@ -51,28 +51,27 @@ function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
|
|||
`
|
||||
})
|
||||
|
||||
// From here on, not reactive—if we're here, we can trust that the
|
||||
// shape in state is a shape with changes that we need to render.
|
||||
const isCurrentParent = useSelector((s) => {
|
||||
return s.data.currentParentId === id
|
||||
})
|
||||
|
||||
const events = useShapeEvents(id, isCurrentParent, rGroup)
|
||||
|
||||
const shape = tld.getShape(state.data, id)
|
||||
|
||||
const shapeUtils = shape ? getShapeUtils(shape) : ({} as ShapeUtility<any>)
|
||||
|
||||
const {
|
||||
isParent = false,
|
||||
isForeignObject = false,
|
||||
canStyleFill = false,
|
||||
} = shapeUtils
|
||||
|
||||
const events = useShapeEvents(id, isParent, rGroup)
|
||||
|
||||
if (!shape) return null
|
||||
|
||||
// From here on, not reactive—if we're here, we can trust that the
|
||||
// shape in state is a shape with changes that we need to render.
|
||||
|
||||
const { isParent, isForeignObject, canStyleFill } = getShapeUtils(shape)
|
||||
|
||||
return (
|
||||
<StyledGroup
|
||||
id={id + '-group'}
|
||||
ref={rGroup}
|
||||
transform={transform}
|
||||
isCurrentParent={isCurrentParent}
|
||||
{...events}
|
||||
>
|
||||
{isSelecting &&
|
||||
|
@ -204,4 +203,24 @@ const EventSoak = styled('use', {
|
|||
|
||||
const StyledGroup = styled('g', {
|
||||
outline: 'none',
|
||||
|
||||
'& > *[data-shy=true]': {
|
||||
opacity: 0,
|
||||
},
|
||||
|
||||
'&:hover': {
|
||||
'& > *[data-shy=true]': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
|
||||
variants: {
|
||||
isCurrentParent: {
|
||||
true: {
|
||||
'& > *[data-shy=true]': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -145,43 +145,39 @@ interface GroupShape extends BaseShape {
|
|||
size: number[]
|
||||
}
|
||||
|
||||
// type DeepPartial<T> = {
|
||||
// [P in keyof T]?: DeepPartial<T[P]>
|
||||
// }
|
||||
|
||||
type ShapeProps<T extends Shape> = {
|
||||
[P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
|
||||
}
|
||||
|
||||
type MutableShape =
|
||||
| DotShape
|
||||
| EllipseShape
|
||||
| LineShape
|
||||
| RayShape
|
||||
| PolylineShape
|
||||
| DrawShape
|
||||
| RectangleShape
|
||||
| ArrowShape
|
||||
| TextShape
|
||||
| GroupShape
|
||||
|
||||
interface Shapes {
|
||||
[ShapeType.Dot]: Readonly<DotShape>
|
||||
[ShapeType.Ellipse]: Readonly<EllipseShape>
|
||||
[ShapeType.Line]: Readonly<LineShape>
|
||||
[ShapeType.Ray]: Readonly<RayShape>
|
||||
[ShapeType.Polyline]: Readonly<PolylineShape>
|
||||
[ShapeType.Draw]: Readonly<DrawShape>
|
||||
[ShapeType.Rectangle]: Readonly<RectangleShape>
|
||||
[ShapeType.Arrow]: Readonly<ArrowShape>
|
||||
[ShapeType.Text]: Readonly<TextShape>
|
||||
[ShapeType.Group]: Readonly<GroupShape>
|
||||
interface MutableShapes {
|
||||
[ShapeType.Dot]: DotShape
|
||||
[ShapeType.Ellipse]: EllipseShape
|
||||
[ShapeType.Line]: LineShape
|
||||
[ShapeType.Ray]: RayShape
|
||||
[ShapeType.Polyline]: PolylineShape
|
||||
[ShapeType.Draw]: DrawShape
|
||||
[ShapeType.Rectangle]: RectangleShape
|
||||
[ShapeType.Arrow]: ArrowShape
|
||||
[ShapeType.Text]: TextShape
|
||||
[ShapeType.Group]: GroupShape
|
||||
}
|
||||
|
||||
type MutableShape = MutableShapes[keyof MutableShapes]
|
||||
|
||||
type Shapes = { [K in keyof MutableShapes]: Readonly<MutableShapes[K]> }
|
||||
|
||||
type Shape = Readonly<MutableShape>
|
||||
|
||||
type ShapeByType<T extends ShapeType> = Shapes[T]
|
||||
|
||||
type IsParent<T> = 'children' extends RequiredKeys<T> ? T : never
|
||||
|
||||
type ParentShape = {
|
||||
[K in keyof MutableShapes]: IsParent<MutableShapes[K]>
|
||||
}[keyof MutableShapes]
|
||||
|
||||
type ParentTypes = ParentShape['type'] & 'page'
|
||||
|
||||
enum Decoration {
|
||||
Arrow = 'Arrow',
|
||||
}
|
||||
|
@ -232,6 +228,15 @@ interface PointerInfo {
|
|||
altKey: boolean
|
||||
}
|
||||
|
||||
interface KeyboardInfo {
|
||||
key: string
|
||||
keys: string[]
|
||||
shiftKey: boolean
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
altKey: boolean
|
||||
}
|
||||
|
||||
enum Edge {
|
||||
Top = 'top_edge',
|
||||
Right = 'right_edge',
|
||||
|
@ -276,8 +281,6 @@ interface BoundsSnapshot extends PointSnapshot {
|
|||
nh: number
|
||||
}
|
||||
|
||||
type Difference<A, B> = A extends B ? never : A
|
||||
|
||||
type ShapeSpecificProps<T extends Shape> = Pick<
|
||||
T,
|
||||
Difference<keyof T, keyof BaseShape>
|
||||
|
@ -561,6 +564,16 @@ interface ShapeUtility<K extends Shape> {
|
|||
shouldRender(this: ShapeUtility<K>, shape: K, previous: K): boolean
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Utilities */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
type Difference<A, B> = A extends B ? never : A
|
||||
|
||||
type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? never : K
|
||||
}[keyof T]
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -695,43 +708,39 @@ interface GroupShape extends BaseShape {
|
|||
size: number[]
|
||||
}
|
||||
|
||||
// type DeepPartial<T> = {
|
||||
// [P in keyof T]?: DeepPartial<T[P]>
|
||||
// }
|
||||
|
||||
type ShapeProps<T extends Shape> = {
|
||||
[P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
|
||||
}
|
||||
|
||||
type MutableShape =
|
||||
| DotShape
|
||||
| EllipseShape
|
||||
| LineShape
|
||||
| RayShape
|
||||
| PolylineShape
|
||||
| DrawShape
|
||||
| RectangleShape
|
||||
| ArrowShape
|
||||
| TextShape
|
||||
| GroupShape
|
||||
|
||||
interface Shapes {
|
||||
[ShapeType.Dot]: Readonly<DotShape>
|
||||
[ShapeType.Ellipse]: Readonly<EllipseShape>
|
||||
[ShapeType.Line]: Readonly<LineShape>
|
||||
[ShapeType.Ray]: Readonly<RayShape>
|
||||
[ShapeType.Polyline]: Readonly<PolylineShape>
|
||||
[ShapeType.Draw]: Readonly<DrawShape>
|
||||
[ShapeType.Rectangle]: Readonly<RectangleShape>
|
||||
[ShapeType.Arrow]: Readonly<ArrowShape>
|
||||
[ShapeType.Text]: Readonly<TextShape>
|
||||
[ShapeType.Group]: Readonly<GroupShape>
|
||||
interface MutableShapes {
|
||||
[ShapeType.Dot]: DotShape
|
||||
[ShapeType.Ellipse]: EllipseShape
|
||||
[ShapeType.Line]: LineShape
|
||||
[ShapeType.Ray]: RayShape
|
||||
[ShapeType.Polyline]: PolylineShape
|
||||
[ShapeType.Draw]: DrawShape
|
||||
[ShapeType.Rectangle]: RectangleShape
|
||||
[ShapeType.Arrow]: ArrowShape
|
||||
[ShapeType.Text]: TextShape
|
||||
[ShapeType.Group]: GroupShape
|
||||
}
|
||||
|
||||
type MutableShape = MutableShapes[keyof MutableShapes]
|
||||
|
||||
type Shapes = { [K in keyof MutableShapes]: Readonly<MutableShapes[K]> }
|
||||
|
||||
type Shape = Readonly<MutableShape>
|
||||
|
||||
type ShapeByType<T extends ShapeType> = Shapes[T]
|
||||
|
||||
type IsParent<T> = 'children' extends RequiredKeys<T> ? T : never
|
||||
|
||||
type ParentShape = {
|
||||
[K in keyof MutableShapes]: IsParent<MutableShapes[K]>
|
||||
}[keyof MutableShapes]
|
||||
|
||||
type ParentTypes = ParentShape['type'] & 'page'
|
||||
|
||||
enum Decoration {
|
||||
Arrow = 'Arrow',
|
||||
}
|
||||
|
@ -782,6 +791,15 @@ interface PointerInfo {
|
|||
altKey: boolean
|
||||
}
|
||||
|
||||
interface KeyboardInfo {
|
||||
key: string
|
||||
keys: string[]
|
||||
shiftKey: boolean
|
||||
ctrlKey: boolean
|
||||
metaKey: boolean
|
||||
altKey: boolean
|
||||
}
|
||||
|
||||
enum Edge {
|
||||
Top = 'top_edge',
|
||||
Right = 'right_edge',
|
||||
|
@ -826,8 +844,6 @@ interface BoundsSnapshot extends PointSnapshot {
|
|||
nh: number
|
||||
}
|
||||
|
||||
type Difference<A, B> = A extends B ? never : A
|
||||
|
||||
type ShapeSpecificProps<T extends Shape> = Pick<
|
||||
T,
|
||||
Difference<keyof T, keyof BaseShape>
|
||||
|
@ -1111,6 +1127,16 @@ interface ShapeUtility<K extends Shape> {
|
|||
shouldRender(this: ShapeUtility<K>, shape: K, previous: K): boolean
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Utilities */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
type Difference<A, B> = A extends B ? never : A
|
||||
|
||||
type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? never : K
|
||||
}[keyof T]
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ export default function useLoadOnMount(roomId?: string) {
|
|||
|
||||
fonts.load('12px Verveine Regular', 'Fonts are loaded!').then(() => {
|
||||
state.send('MOUNTED')
|
||||
|
||||
if (roomId !== undefined) {
|
||||
state.send('RT_LOADED_ROOM', { id: roomId })
|
||||
}
|
||||
|
|
|
@ -6,14 +6,15 @@ import Vec from 'utils/vec'
|
|||
|
||||
export default function useShapeEvents(
|
||||
id: string,
|
||||
isParent: boolean,
|
||||
isCurrentParent: boolean,
|
||||
rGroup: MutableRefObject<SVGElement>
|
||||
) {
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (isParent) return
|
||||
if (isCurrentParent) return
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
e.stopPropagation()
|
||||
|
||||
rGroup.current.setPointerCapture(e.pointerId)
|
||||
|
||||
const info = inputs.pointerDown(e, id)
|
||||
|
@ -28,30 +29,30 @@ export default function useShapeEvents(
|
|||
state.send('RIGHT_POINTED', info)
|
||||
}
|
||||
},
|
||||
[id]
|
||||
[id, isCurrentParent]
|
||||
)
|
||||
|
||||
const handlePointerUp = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (isCurrentParent) return
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
e.stopPropagation()
|
||||
|
||||
rGroup.current.releasePointerCapture(e.pointerId)
|
||||
state.send('STOPPED_POINTING', inputs.pointerUp(e, id))
|
||||
},
|
||||
[id]
|
||||
[id, isCurrentParent]
|
||||
)
|
||||
|
||||
const handlePointerEnter = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (isCurrentParent) return
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
e.stopPropagation()
|
||||
|
||||
if (isParent) {
|
||||
state.send('HOVERED_GROUP', inputs.pointerEnter(e, id))
|
||||
} else {
|
||||
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
||||
}
|
||||
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
||||
},
|
||||
[id]
|
||||
[id, isCurrentParent]
|
||||
)
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
|
@ -72,26 +73,22 @@ export default function useShapeEvents(
|
|||
return
|
||||
}
|
||||
|
||||
if (isParent) {
|
||||
state.send('MOVED_OVER_GROUP', inputs.pointerEnter(e, id))
|
||||
} else {
|
||||
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
||||
}
|
||||
if (isCurrentParent) return
|
||||
|
||||
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
||||
},
|
||||
[id]
|
||||
[id, isCurrentParent]
|
||||
)
|
||||
|
||||
const handlePointerLeave = useCallback(
|
||||
(e: React.PointerEvent) => {
|
||||
if (isCurrentParent) return
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
e.stopPropagation()
|
||||
|
||||
if (isParent) {
|
||||
state.send('UNHOVERED_GROUP', { target: id })
|
||||
} else {
|
||||
state.send('UNHOVERED_SHAPE', { target: id })
|
||||
}
|
||||
state.send('UNHOVERED_SHAPE', { target: id })
|
||||
},
|
||||
[id]
|
||||
[id, isCurrentParent]
|
||||
)
|
||||
|
||||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"start": "next start",
|
||||
"test-all": "yarn lint && yarn type-check && yarn test",
|
||||
"test:update": "jest --updateSnapshot",
|
||||
"test:watch": "jest --watchAll --verbose=false --silent=false",
|
||||
"test:watch": "jest --watchAll",
|
||||
"test": "jest --watchAll=false",
|
||||
"type-check": "tsc --pretty --noEmit"
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { ArrowShape, Decoration, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
TextCodeControl,
|
||||
VectorCodeControl,
|
||||
} from 'types'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
|
||||
export const controls: Record<string, any> = {}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { DotShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { DrawShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { EllipseShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { LineShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { PolylineShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { RayShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { RectangleShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import CodeShape from './index'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { TextShape, ShapeProps, ShapeType } from 'types'
|
||||
import { defaultStyle } from 'state/shape-styles'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
|
|
|
@ -16,10 +16,12 @@ export default function changePage(data: Data, toPageId: string): void {
|
|||
storage.savePage(data, data.document.id, fromPageId)
|
||||
storage.loadPage(data, data.document.id, toPageId)
|
||||
data.currentPageId = toPageId
|
||||
data.currentParentId = toPageId
|
||||
},
|
||||
undo(data) {
|
||||
storage.loadPage(data, data.document.id, fromPageId)
|
||||
data.currentPageId = fromPageId
|
||||
data.currentParentId = fromPageId
|
||||
},
|
||||
})
|
||||
)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Command from './command'
|
||||
import history from '../history'
|
||||
import { Data, Page, PageState } from 'types'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import storage from 'state/storage'
|
||||
|
||||
export default function createPage(data: Data, goToPage = true): void {
|
||||
|
|
|
@ -3,7 +3,7 @@ import history from '../history'
|
|||
import { Data } from 'types'
|
||||
import { deepClone } from 'utils'
|
||||
import tld from 'utils/tld'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
|
||||
export default function duplicateCommand(data: Data): void {
|
||||
|
|
|
@ -57,6 +57,7 @@ export default function moveCommand(data: Data, type: MoveType): void {
|
|||
.sort((a, b) => b.childIndex - a.childIndex)
|
||||
.forEach((shape) => moveForward(shape, siblings, visited))
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
case MoveType.Backward: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import history from '../history'
|
|||
import { Data, Shape } from 'types'
|
||||
import { getCommonBounds, setToArray } from 'utils'
|
||||
import tld from 'utils/tld'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import state from 'state/state'
|
||||
|
|
|
@ -6,7 +6,7 @@ import {
|
|||
MyPresenceCallback,
|
||||
OthersEventCallback,
|
||||
} from '@liveblocks/client/lib/cjs/types'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
|
||||
class CoopClient {
|
||||
id = uniqueId()
|
||||
|
|
|
@ -160,6 +160,7 @@ class Inputs {
|
|||
}
|
||||
|
||||
this.pointer = info
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Data, GroupShape, Shape, ShapeType } from 'types'
|
|||
import vec from 'utils/vec'
|
||||
import BaseSession from './base-session'
|
||||
import commands from 'state/commands'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import tld from 'utils/tld'
|
||||
|
||||
|
|
|
@ -1,12 +1,16 @@
|
|||
import { getArcLength, uniqueId } from 'utils'
|
||||
import vec from 'utils/vec'
|
||||
import {
|
||||
getArcLength,
|
||||
uniqueId,
|
||||
getSvgPathFromStroke,
|
||||
rng,
|
||||
getBoundsFromPoints,
|
||||
translateBounds,
|
||||
pointInBounds,
|
||||
pointInCircle,
|
||||
circleFromThreePoints,
|
||||
isAngleBetween,
|
||||
getPerfectDashProps,
|
||||
} from 'utils'
|
||||
import {
|
||||
ArrowShape,
|
||||
|
@ -15,7 +19,6 @@ import {
|
|||
ShapeHandle,
|
||||
ShapeType,
|
||||
} from 'types'
|
||||
import { circleFromThreePoints, isAngleBetween } from 'utils'
|
||||
import {
|
||||
intersectArcBounds,
|
||||
intersectLineSegmentBounds,
|
||||
|
@ -24,7 +27,6 @@ import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
|||
import getStroke from 'perfect-freehand'
|
||||
import React from 'react'
|
||||
import { registerShapeUtils } from './register'
|
||||
import { getPerfectDashProps } from 'utils/dashes'
|
||||
|
||||
const pathCache = new WeakMap<ArrowShape, string>([])
|
||||
|
||||
|
@ -37,55 +39,65 @@ function getCtp(shape: ArrowShape) {
|
|||
const arrow = registerShapeUtils<ArrowShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Arrow,
|
||||
isGenerated: false,
|
||||
name: 'Arrow',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
bend: 0,
|
||||
handles: {
|
||||
start: {
|
||||
id: 'start',
|
||||
index: 0,
|
||||
point: [0, 0],
|
||||
},
|
||||
end: {
|
||||
id: 'end',
|
||||
index: 1,
|
||||
point: [1, 1],
|
||||
},
|
||||
bend: {
|
||||
id: 'bend',
|
||||
index: 2,
|
||||
point: [0.5, 0.5],
|
||||
},
|
||||
},
|
||||
decorations: {
|
||||
start: null,
|
||||
middle: null,
|
||||
end: Decoration.Arrow,
|
||||
},
|
||||
style: {
|
||||
...defaultStyle,
|
||||
isFilled: false,
|
||||
},
|
||||
},
|
||||
|
||||
create(props) {
|
||||
const {
|
||||
point = [0, 0],
|
||||
handles = {
|
||||
start: {
|
||||
id: 'start',
|
||||
index: 0,
|
||||
point: [0, 0],
|
||||
},
|
||||
end: {
|
||||
id: 'end',
|
||||
index: 1,
|
||||
point: [1, 1],
|
||||
},
|
||||
bend: {
|
||||
id: 'bend',
|
||||
index: 2,
|
||||
point: [0.5, 0.5],
|
||||
},
|
||||
},
|
||||
} = props
|
||||
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Arrow,
|
||||
isGenerated: false,
|
||||
name: 'Arrow',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point,
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
bend: 0,
|
||||
handles,
|
||||
decorations: {
|
||||
start: null,
|
||||
middle: null,
|
||||
end: Decoration.Arrow,
|
||||
},
|
||||
const shape = {
|
||||
...this.defaultProps,
|
||||
...props,
|
||||
decorations: {
|
||||
...this.defaultProps.decorations,
|
||||
...props.decorations,
|
||||
},
|
||||
style: {
|
||||
...defaultStyle,
|
||||
...this.defaultProps.style,
|
||||
...props.style,
|
||||
isFilled: false,
|
||||
},
|
||||
}
|
||||
|
||||
// shape.handles.bend.point = getBendPoint(shape)
|
||||
|
||||
return shape
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import { DotShape, ShapeType } from 'types'
|
||||
import { intersectCircleBounds } from 'utils/intersections'
|
||||
import { boundsContained, translateBounds } from 'utils'
|
||||
|
@ -8,27 +8,19 @@ import { registerShapeUtils } from './register'
|
|||
const dot = registerShapeUtils<DotShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Dot,
|
||||
isGenerated: false,
|
||||
name: 'Dot',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
...props,
|
||||
style: {
|
||||
...defaultStyle,
|
||||
...props.style,
|
||||
isFilled: false,
|
||||
},
|
||||
}
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Dot,
|
||||
isGenerated: false,
|
||||
name: 'Dot',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
},
|
||||
|
||||
render({ id }) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
|
||||
import { intersectPolylineBounds } from 'utils/intersections'
|
||||
|
@ -22,27 +22,20 @@ const draw = registerShapeUtils<DrawShape>({
|
|||
|
||||
canStyleFill: true,
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Draw,
|
||||
isGenerated: false,
|
||||
name: 'Draw',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
points: [],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
...props,
|
||||
style: {
|
||||
...defaultStyle,
|
||||
...props.style,
|
||||
},
|
||||
}
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Draw,
|
||||
isGenerated: false,
|
||||
name: 'Draw',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
points: [],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import { getPerfectDashProps } from 'utils/dashes'
|
||||
import vec from 'utils/vec'
|
||||
import { DashStyle, EllipseShape, ShapeType } from 'types'
|
||||
import { getShapeUtils } from './index'
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
pointInEllipse,
|
||||
boundsContained,
|
||||
getRotatedEllipseBounds,
|
||||
getPerfectDashProps,
|
||||
} from 'utils'
|
||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||
import getStroke from 'perfect-freehand'
|
||||
|
@ -21,25 +21,21 @@ const pathCache = new WeakMap<EllipseShape, string>([])
|
|||
const ellipse = registerShapeUtils<EllipseShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Ellipse,
|
||||
isGenerated: false,
|
||||
name: 'Ellipse',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
radiusX: 1,
|
||||
radiusY: 1,
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
...props,
|
||||
}
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Ellipse,
|
||||
isGenerated: false,
|
||||
name: 'Ellipse',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
radiusX: 1,
|
||||
radiusY: 1,
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { GroupShape, ShapeType } from 'types'
|
||||
import { getShapeUtils } from './index'
|
||||
|
@ -12,26 +12,21 @@ const group = registerShapeUtils<GroupShape>({
|
|||
isShy: true,
|
||||
isParent: true,
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Group,
|
||||
isGenerated: false,
|
||||
name: 'Group',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
size: [1, 1],
|
||||
radius: 2,
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
children: [],
|
||||
...props,
|
||||
}
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Group,
|
||||
isGenerated: false,
|
||||
name: 'Group',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
size: [1, 1],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
children: [],
|
||||
},
|
||||
|
||||
render(shape) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { LineShape, ShapeType } from 'types'
|
||||
import { intersectCircleBounds } from 'utils/intersections'
|
||||
|
@ -10,28 +10,20 @@ import { registerShapeUtils } from './register'
|
|||
const line = registerShapeUtils<LineShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Line,
|
||||
isGenerated: false,
|
||||
name: 'Line',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
direction: [0, 0],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
...props,
|
||||
style: {
|
||||
...defaultStyle,
|
||||
...props.style,
|
||||
isFilled: false,
|
||||
},
|
||||
}
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Line,
|
||||
isGenerated: false,
|
||||
name: 'Line',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
direction: [0, 0],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { PolylineShape, ShapeType } from 'types'
|
||||
import { intersectPolylineBounds } from 'utils/intersections'
|
||||
|
@ -13,24 +13,20 @@ import { registerShapeUtils } from './register'
|
|||
const polyline = registerShapeUtils<PolylineShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Polyline,
|
||||
isGenerated: false,
|
||||
name: 'Polyline',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
points: [[0, 0]],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
...props,
|
||||
}
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Polyline,
|
||||
isGenerated: false,
|
||||
name: 'Polyline',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
points: [[0, 0]],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { RayShape, ShapeType } from 'types'
|
||||
import { intersectCircleBounds } from 'utils/intersections'
|
||||
|
@ -10,28 +10,20 @@ import { registerShapeUtils } from './register'
|
|||
const ray = registerShapeUtils<RayShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Ray,
|
||||
isGenerated: false,
|
||||
name: 'Ray',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
direction: [0, 1],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
...props,
|
||||
style: {
|
||||
...defaultStyle,
|
||||
...props.style,
|
||||
isFilled: false,
|
||||
},
|
||||
}
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Ray,
|
||||
isGenerated: false,
|
||||
name: 'Ray',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
direction: [0, 1],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
|
|
|
@ -1,36 +1,32 @@
|
|||
import { uniqueId } from 'utils'
|
||||
import { uniqueId, getPerfectDashProps } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { DashStyle, RectangleShape, ShapeType } from 'types'
|
||||
import { getSvgPathFromStroke, translateBounds, rng, shuffleArr } from 'utils'
|
||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||
import getStroke from 'perfect-freehand'
|
||||
import { registerShapeUtils } from './register'
|
||||
import { getPerfectDashProps } from 'utils/dashes'
|
||||
|
||||
const pathCache = new WeakMap<number[], string>([])
|
||||
|
||||
const rectangle = registerShapeUtils<RectangleShape>({
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
|
||||
type: ShapeType.Rectangle,
|
||||
isGenerated: false,
|
||||
name: 'Rectangle',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
size: [1, 1],
|
||||
radius: 2,
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
...props,
|
||||
}
|
||||
type: ShapeType.Rectangle,
|
||||
isGenerated: false,
|
||||
name: 'Rectangle',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
size: [1, 1],
|
||||
radius: 2,
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Shape, ShapeUtility } from 'types'
|
||||
import vec from 'utils/vec'
|
||||
import React from 'react'
|
||||
import {
|
||||
vec,
|
||||
pointInBounds,
|
||||
getBoundsCenter,
|
||||
getBoundsFromPoints,
|
||||
|
@ -8,8 +8,7 @@ import {
|
|||
boundsCollidePolygon,
|
||||
boundsContainPolygon,
|
||||
} from 'utils'
|
||||
import { uniqueId } from 'utils'
|
||||
import React from 'react'
|
||||
import { Shape, ShapeUtility } from 'types'
|
||||
|
||||
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||
return {
|
||||
|
@ -22,19 +21,19 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
|||
isParent: false,
|
||||
isForeignObject: false,
|
||||
|
||||
defaultProps: {} as T,
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
isGenerated: false,
|
||||
point: [0, 0],
|
||||
name: 'Shape',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
...this.defaultProps,
|
||||
...props,
|
||||
style: {
|
||||
...this.defaultProps.style,
|
||||
...props.style,
|
||||
isFilled: this.canStyleFill
|
||||
? props.style?.isFilled || this.defaultProps.style.isFilled
|
||||
: false,
|
||||
},
|
||||
} as T
|
||||
},
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { uniqueId, isMobile } from 'utils'
|
||||
import { uniqueId, isMobile } from 'utils/utils'
|
||||
import vec from 'utils/vec'
|
||||
import { TextShape, ShapeType } from 'types'
|
||||
import {
|
||||
|
@ -47,27 +47,23 @@ const text = registerShapeUtils<TextShape>({
|
|||
isForeignObject: true,
|
||||
canChangeAspectRatio: false,
|
||||
canEdit: true,
|
||||
|
||||
boundsCache: new WeakMap([]),
|
||||
|
||||
create(props) {
|
||||
return {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Text,
|
||||
isGenerated: false,
|
||||
name: 'Text',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
text: '',
|
||||
scale: 1,
|
||||
...props,
|
||||
}
|
||||
defaultProps: {
|
||||
id: uniqueId(),
|
||||
type: ShapeType.Text,
|
||||
isGenerated: false,
|
||||
name: 'Text',
|
||||
parentId: 'page1',
|
||||
childIndex: 0,
|
||||
point: [0, 0],
|
||||
rotation: 0,
|
||||
isAspectRatioLocked: false,
|
||||
isLocked: false,
|
||||
isHidden: false,
|
||||
style: defaultStyle,
|
||||
text: '',
|
||||
scale: 1,
|
||||
},
|
||||
|
||||
shouldRender(shape, prev) {
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import { createSelectorHook, createState } from '@state-designer/react'
|
||||
import { updateFromCode } from './code/generate'
|
||||
import { createShape, getShapeUtils } from './shape-utils'
|
||||
import vec from 'utils/vec'
|
||||
import * as Sessions from './sessions'
|
||||
import inputs from './inputs'
|
||||
import history from './history'
|
||||
import storage from './storage'
|
||||
import session from './session'
|
||||
import clipboard from './clipboard'
|
||||
import * as Sessions from './sessions'
|
||||
import coopClient from './coop/client-liveblocks'
|
||||
import commands from './commands'
|
||||
import {
|
||||
vec,
|
||||
getCommonBounds,
|
||||
rotateBounds,
|
||||
getBoundsCenter,
|
||||
|
@ -18,7 +19,7 @@ import {
|
|||
pointInBounds,
|
||||
uniqueId,
|
||||
} from 'utils'
|
||||
import tld from 'utils/tld'
|
||||
import tld from '../utils/tld'
|
||||
import {
|
||||
Data,
|
||||
PointerInfo,
|
||||
|
@ -36,7 +37,6 @@ import {
|
|||
SizeStyle,
|
||||
ColorStyle,
|
||||
} from 'types'
|
||||
import session from './session'
|
||||
|
||||
const initialData: Data = {
|
||||
isReadOnly: false,
|
||||
|
@ -288,10 +288,15 @@ const state = createState({
|
|||
unless: 'isInSession',
|
||||
do: ['loadDocumentFromJson', 'resetHistory'],
|
||||
},
|
||||
DESELECTED_ALL: {
|
||||
unless: 'isInSession',
|
||||
do: 'deselectAll',
|
||||
to: 'selecting',
|
||||
},
|
||||
SELECTED_ALL: {
|
||||
unless: 'isInSession',
|
||||
to: 'selecting',
|
||||
do: 'selectAll',
|
||||
to: 'selecting',
|
||||
},
|
||||
CHANGED_PAGE: {
|
||||
unless: 'isInSession',
|
||||
|
@ -398,8 +403,15 @@ const state = createState({
|
|||
notPointing: {
|
||||
onEnter: 'clearPointedId',
|
||||
on: {
|
||||
CANCELLED: 'clearSelectedIds',
|
||||
POINTED_CANVAS: { to: 'brushSelecting' },
|
||||
CANCELLED: {
|
||||
if: 'hasCurrentParentShape',
|
||||
do: ['selectCurrentParentId', 'raiseCurrentParentId'],
|
||||
else: 'clearSelectedIds',
|
||||
},
|
||||
POINTED_CANVAS: {
|
||||
to: 'brushSelecting',
|
||||
do: 'setCurrentParentIdToPage',
|
||||
},
|
||||
POINTED_BOUNDS: [
|
||||
{
|
||||
if: 'isPressingMetaKey',
|
||||
|
@ -477,7 +489,7 @@ const state = createState({
|
|||
{
|
||||
unless: 'isPressingShiftKey',
|
||||
do: [
|
||||
'setDrilledPointedId',
|
||||
'setCurrentParentId',
|
||||
'clearSelectedIds',
|
||||
'pushPointedIdToSelectedIds',
|
||||
],
|
||||
|
@ -1120,6 +1132,9 @@ const state = createState({
|
|||
hasMultipleSelection(data) {
|
||||
return tld.getSelectedIds(data).size > 1
|
||||
},
|
||||
hasCurrentParentShape(data) {
|
||||
return data.currentParentId !== data.currentPageId
|
||||
},
|
||||
isToolLocked(data) {
|
||||
return data.settings.isToolLocked
|
||||
},
|
||||
|
@ -1180,6 +1195,14 @@ const state = createState({
|
|||
|
||||
data.currentPageId = newId
|
||||
|
||||
data.pointedId = null
|
||||
data.hoveredId = null
|
||||
data.editingId = null
|
||||
data.currentPageId = 'page1'
|
||||
data.currentParentId = 'page1'
|
||||
data.currentCodeFileId = 'file0'
|
||||
data.codeControls = {}
|
||||
|
||||
data.document.pages = {
|
||||
[newId]: {
|
||||
id: newId,
|
||||
|
@ -1234,6 +1257,7 @@ const state = createState({
|
|||
|
||||
createShape(data, payload, type: ShapeType) {
|
||||
const shape = createShape(type, {
|
||||
id: uniqueId(),
|
||||
parentId: data.currentPageId,
|
||||
point: vec.round(tld.screenToWorld(payload.point, data)),
|
||||
style: deepClone(data.currentStyle),
|
||||
|
@ -1500,11 +1524,12 @@ const state = createState({
|
|||
)
|
||||
)
|
||||
},
|
||||
|
||||
clearInputs() {
|
||||
inputs.clear()
|
||||
},
|
||||
|
||||
deselectAll(data) {
|
||||
tld.getSelectedIds(data).clear()
|
||||
},
|
||||
selectAll(data) {
|
||||
const selectedIds = tld.getSelectedIds(data)
|
||||
const page = tld.getPage(data)
|
||||
|
@ -1525,10 +1550,24 @@ const state = createState({
|
|||
data.pointedId = getPointedId(data, payload.target)
|
||||
data.currentParentId = getParentId(data, data.pointedId)
|
||||
},
|
||||
setDrilledPointedId(data, payload: PointerInfo) {
|
||||
setCurrentParentId(data, payload: PointerInfo) {
|
||||
data.pointedId = getDrilledPointedId(data, payload.target)
|
||||
data.currentParentId = getParentId(data, data.pointedId)
|
||||
},
|
||||
raiseCurrentParentId(data) {
|
||||
const currentParent = tld.getShape(data, data.currentParentId)
|
||||
|
||||
data.currentParentId =
|
||||
currentParent.parentId === data.currentPageId
|
||||
? data.currentPageId
|
||||
: currentParent.parentId
|
||||
},
|
||||
setCurrentParentIdToPage(data) {
|
||||
data.currentParentId = data.currentPageId
|
||||
},
|
||||
selectCurrentParentId(data) {
|
||||
tld.setSelectedIds(data, [data.currentParentId])
|
||||
},
|
||||
clearCurrentParentId(data) {
|
||||
data.currentParentId = data.currentPageId
|
||||
data.pointedId = undefined
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Data, PageState, TLDocument } from 'types'
|
||||
import { decompress, compress, setToArray } from 'utils'
|
||||
import state from './state'
|
||||
import { uniqueId } from 'utils'
|
||||
import { uniqueId } from 'utils/utils'
|
||||
import * as idb from 'idb-keyval'
|
||||
|
||||
const CURRENT_VERSION = 'code_slate_0.0.8'
|
||||
|
|
5
types.ts
5
types.ts
|
@ -458,6 +458,9 @@ export type PropsOfType<T extends Record<string, unknown>> = {
|
|||
export type Mutable<T extends Shape> = { -readonly [K in keyof T]: T[K] }
|
||||
|
||||
export interface ShapeUtility<K extends Shape> {
|
||||
// Default properties when creating a new shape
|
||||
defaultProps: K
|
||||
|
||||
// A cache for the computed bounds of this kind of shape.
|
||||
boundsCache: WeakMap<K, Bounds>
|
||||
|
||||
|
@ -483,7 +486,7 @@ export interface ShapeUtility<K extends Shape> {
|
|||
isShy: boolean
|
||||
|
||||
// Create a new shape.
|
||||
create(props: Partial<K>): K
|
||||
create(this: ShapeUtility<K>, props: Partial<K>): K
|
||||
|
||||
// Update a shape's styles
|
||||
applyStyles(
|
||||
|
|
|
@ -1,41 +0,0 @@
|
|||
/**
|
||||
* Get balanced dash-strokearray and dash-strokeoffset properties for a path of a given length.
|
||||
* @param length The length of the path.
|
||||
* @param strokeWidth The shape's stroke-width property.
|
||||
* @param style The stroke's style: "dashed" or "dotted" (default "dashed").
|
||||
* @param snap An interval for dashes (e.g. 4 will produce arrays with 4, 8, 16, etc dashes).
|
||||
*/
|
||||
export function getPerfectDashProps(
|
||||
length: number,
|
||||
strokeWidth: number,
|
||||
style: 'dashed' | 'dotted' = 'dashed',
|
||||
snap = 1
|
||||
): {
|
||||
strokeDasharray: string
|
||||
strokeDashoffset: string
|
||||
} {
|
||||
let dashLength: number
|
||||
let strokeDashoffset: string
|
||||
let ratio: number
|
||||
|
||||
if (style === 'dashed') {
|
||||
dashLength = strokeWidth * 2
|
||||
ratio = 1
|
||||
strokeDashoffset = (dashLength / 2).toString()
|
||||
} else {
|
||||
dashLength = strokeWidth / 100
|
||||
ratio = 100
|
||||
strokeDashoffset = '0'
|
||||
}
|
||||
|
||||
let dashes = Math.floor(length / dashLength / (2 * ratio))
|
||||
dashes -= dashes % snap
|
||||
if (dashes === 0) dashes = 1
|
||||
|
||||
const gapLength = (length - dashes * dashLength) / dashes
|
||||
|
||||
return {
|
||||
strokeDasharray: [dashLength, gapLength].join(' '),
|
||||
strokeDashoffset,
|
||||
}
|
||||
}
|
|
@ -1 +1,5 @@
|
|||
import vec from './vec'
|
||||
import svg from './svg'
|
||||
export * from './utils'
|
||||
|
||||
export { vec, svg }
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { clamp, deepClone, getCommonBounds, setToArray } from './utils'
|
||||
import { clamp, deepClone, getCommonBounds, setToArray } from 'utils'
|
||||
import { getShapeUtils } from 'state/shape-utils'
|
||||
import vec from './vec'
|
||||
import {
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
} from 'types'
|
||||
import { AssertionError } from 'assert'
|
||||
|
||||
export default class ProjectUtils {
|
||||
export default class StateUtils {
|
||||
static getCameraZoom(zoom: number): number {
|
||||
return clamp(zoom, 0.1, 5)
|
||||
}
|
||||
|
|
1442
utils/utils.ts
1442
utils/utils.ts
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue