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', () => {
|
describe('arrow shape', () => {
|
||||||
it('creates a shape', () => {
|
it('creates a shape', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cancels shape while creating', () => {
|
it('cancels shape while creating', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('removes shape on undo and restores it on redo', () => {
|
it('removes shape on undo and restores it on redo', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not create shape when readonly', () => {
|
it('does not create shape when readonly', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getPerfectDashProps } from 'utils/dashes'
|
import { getPerfectDashProps } from 'utils'
|
||||||
|
|
||||||
describe('ellipse dash props', () => {
|
describe('ellipse dash props', () => {
|
||||||
it('renders dashed props on a circle correctly', () => {
|
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 { ShapeType } from 'types'
|
||||||
import {
|
import TestState, { rectangleId, arrowId } from './test-utils'
|
||||||
idsAreSelected,
|
|
||||||
point,
|
|
||||||
rectangleId,
|
|
||||||
arrowId,
|
|
||||||
getOnlySelectedShape,
|
|
||||||
assertShapeProps,
|
|
||||||
} from './test-utils'
|
|
||||||
import tld from 'utils/tld'
|
|
||||||
import * as json from './__mocks__/document.json'
|
|
||||||
|
|
||||||
describe('deleting single shapes', () => {
|
describe('deleting single shapes', () => {
|
||||||
state.reset()
|
const tt = new TestState()
|
||||||
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
|
||||||
|
|
||||||
it('deletes a shape and undoes the delete', () => {
|
describe('deleting single shapes', () => {
|
||||||
state
|
it('deletes a shape and undoes the delete', () => {
|
||||||
.send('CANCELED')
|
tt.deselectAll().clickShape(rectangleId).pressDelete()
|
||||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
|
||||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
|
||||||
|
|
||||||
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)
|
tt.undo()
|
||||||
expect(tld.getShape(state.data, rectangleId)).toBe(undefined)
|
|
||||||
|
|
||||||
state.send('UNDO')
|
expect(tt.getShape(rectangleId)).toBeTruthy()
|
||||||
|
expect(tt.idsAreSelected([rectangleId])).toBe(true)
|
||||||
|
|
||||||
expect(tld.getShape(state.data, rectangleId)).toBeTruthy()
|
tt.redo()
|
||||||
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
|
||||||
|
|
||||||
state.send('REDO')
|
expect(tt.getShape(rectangleId)).toBe(undefined)
|
||||||
|
})
|
||||||
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 }))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// it('selects the new group', () => {
|
describe('deleting and restoring grouped shapes', () => {
|
||||||
// expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
it('creates a group', () => {
|
||||||
// })
|
tt.reset()
|
||||||
|
.deselectAll()
|
||||||
|
.clickShape(rectangleId)
|
||||||
|
.clickShape(arrowId, { shiftKey: true })
|
||||||
|
.send('GROUPED')
|
||||||
|
|
||||||
// it('assigns a new parent', () => {
|
const group = tt.getOnlySelectedShape()
|
||||||
// expect(groupId === state.data.currentPageId).toBe(false)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // Rectangle has the same new parent?
|
// Should select the group
|
||||||
// it('assigns new parent to all selected shapes', () => {
|
expect(tt.assertShapeProps(group, { type: ShapeType.Group })).toBe(true)
|
||||||
// expect(hasParent(state.data, arrowId, groupId)).toBe(true)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// // New parent is selected?
|
const arrow = tt.getShape(arrowId)
|
||||||
// it('selects the new parent', () => {
|
|
||||||
// expect(idsAreSelected(state.data, [groupId])).toBe(true)
|
// 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', () => {
|
it('mounts the state', () => {
|
||||||
state.send('MOUNTED')
|
state.send('MOUNTED')
|
||||||
|
|
||||||
expect(state.isIn('ready')).toBe(true)
|
expect(state.isIn('ready')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('loads file from json', () => {
|
it('loads file from json', () => {
|
||||||
state.send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
state.send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||||
|
|
||||||
expect(state.isIn('ready')).toBe(true)
|
expect(state.isIn('ready')).toBe(true)
|
||||||
expect(state.data.document).toMatchSnapshot('data after mount from file')
|
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 TestState, { rectangleId, arrowId } from './test-utils'
|
||||||
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) })
|
|
||||||
|
|
||||||
describe('selection', () => {
|
describe('selection', () => {
|
||||||
it('selects a shape', () => {
|
const tt = new TestState()
|
||||||
state
|
|
||||||
.send('CANCELED')
|
|
||||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
|
||||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
|
||||||
|
|
||||||
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', () => {
|
it('selects and deselects a shape', () => {
|
||||||
state
|
tt.deselectAll().clickShape(rectangleId).clickCanvas()
|
||||||
.send('CANCELED')
|
|
||||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
|
||||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
|
||||||
|
|
||||||
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
|
expect(tt.idsAreSelected([])).toBe(true)
|
||||||
|
|
||||||
state
|
|
||||||
.send('POINTED_CANVAS', inputs.pointerDown(point(), 'canvas'))
|
|
||||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), 'canvas'))
|
|
||||||
|
|
||||||
expect(idsAreSelected(state.data, [])).toBe(true)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('selects multiple shapes', () => {
|
it('selects multiple shapes', () => {
|
||||||
expect(idsAreSelected(state.data, [])).toBe(true)
|
tt.deselectAll()
|
||||||
|
.clickShape(rectangleId)
|
||||||
|
.clickShape(arrowId, { shiftKey: true })
|
||||||
|
|
||||||
state
|
expect(tt.idsAreSelected([rectangleId, arrowId])).toBe(true)
|
||||||
.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)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shift-selects to deselect shapes', () => {
|
it('shift-selects to deselect shapes', () => {
|
||||||
state
|
tt.deselectAll()
|
||||||
.send('CANCELLED')
|
.clickShape(rectangleId)
|
||||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
.clickShape(arrowId, { shiftKey: true })
|
||||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
.clickShape(rectangleId, { shiftKey: true })
|
||||||
.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)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(idsAreSelected(state.data, [arrowId])).toBe(true)
|
expect(tt.idsAreSelected([arrowId])).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('single-selects shape in selection on pointerup', () => {
|
it('single-selects shape in selection on click', () => {
|
||||||
state
|
tt.deselectAll()
|
||||||
.send('CANCELLED')
|
.clickShape(rectangleId)
|
||||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
.clickShape(arrowId, { shiftKey: true })
|
||||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
.clickShape(arrowId)
|
||||||
.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([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', () => {
|
it('selects shapes if shift key is lifted before pointerup', () => {
|
||||||
state
|
tt.deselectAll()
|
||||||
.send('CANCELLED')
|
.clickShape(rectangleId)
|
||||||
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
|
.clickShape(arrowId, { shiftKey: true })
|
||||||
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
|
.startClick(rectangleId, { shiftKey: true })
|
||||||
.send(
|
.stopClick(rectangleId)
|
||||||
'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))
|
|
||||||
|
|
||||||
expect(idsAreSelected(state.data, [arrowId])).toBe(true)
|
expect(tt.idsAreSelected([rectangleId])).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('does not select on meta-click', () => {
|
it('does not select on meta-click', () => {
|
||||||
state
|
tt.deselectAll().clickShape(rectangleId, { ctrlKey: true })
|
||||||
.send('CANCELLED')
|
|
||||||
.send(
|
|
||||||
'POINTED_SHAPE',
|
|
||||||
inputs.pointerDown(point({ ctrlKey: true }), rectangleId)
|
|
||||||
)
|
|
||||||
.send(
|
|
||||||
'STOPPED_POINTING',
|
|
||||||
inputs.pointerUp(point({ ctrlKey: true }), rectangleId)
|
|
||||||
)
|
|
||||||
|
|
||||||
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', () => {
|
describe('arrow shape', () => {
|
||||||
it('creates shape', () => {
|
it('creates shape', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('cancels shape while creating', () => {
|
it('cancels shape while creating', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('moves shape', () => {
|
it('moves shape', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('rotates shape', () => {
|
it('rotates shape', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('measures bounds', () => {
|
it('rotates shape in a group', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('measures rotated bounds', () => {
|
it('measures shape bounds', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('transforms single', () => {
|
it('measures shape rotated bounds', () => {
|
||||||
|
// TODO
|
||||||
|
null
|
||||||
|
})
|
||||||
|
|
||||||
|
it('transforms single shape', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('transforms in a group', () => {
|
it('transforms in a group', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
/* -------------------- Specific -------------------- */
|
/* -------------------- Specific -------------------- */
|
||||||
|
|
||||||
it('creates compass-aligned shape with shift key', () => {
|
it('creates compass-aligned shape with shift key', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('changes start handle', () => {
|
it('changes start handle', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('changes end handle', () => {
|
it('changes end handle', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('changes bend handle', () => {
|
it('changes bend handle', () => {
|
||||||
|
// TODO
|
||||||
null
|
null
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resets bend handle when double-pointed', () => {
|
it('resets bend handle when double-pointed', () => {
|
||||||
null
|
// TODO
|
||||||
})
|
|
||||||
|
|
||||||
/* -------------------- Readonly -------------------- */
|
|
||||||
|
|
||||||
it('does not create shape when readonly', () => {
|
|
||||||
null
|
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 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 rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
|
||||||
export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
|
export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
|
||||||
|
|
||||||
interface PointerOptions {
|
interface PointerOptions {
|
||||||
id?: string
|
id?: number
|
||||||
x?: number
|
x?: number
|
||||||
y?: number
|
y?: number
|
||||||
shiftKey?: boolean
|
shiftKey?: boolean
|
||||||
|
@ -13,77 +20,573 @@ interface PointerOptions {
|
||||||
ctrlKey?: boolean
|
ctrlKey?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export function point(
|
class TestState {
|
||||||
options: PointerOptions = {} as PointerOptions
|
state: State
|
||||||
): PointerEvent {
|
|
||||||
const {
|
|
||||||
id = '1',
|
|
||||||
x = 0,
|
|
||||||
y = 0,
|
|
||||||
shiftKey = false,
|
|
||||||
altKey = false,
|
|
||||||
ctrlKey = false,
|
|
||||||
} = options
|
|
||||||
|
|
||||||
return {
|
constructor() {
|
||||||
shiftKey,
|
this.state = _state
|
||||||
altKey,
|
this.reset()
|
||||||
ctrlKey,
|
}
|
||||||
pointerId: id,
|
|
||||||
clientX: x,
|
|
||||||
clientY: y,
|
|
||||||
} as any
|
|
||||||
}
|
|
||||||
|
|
||||||
export function idsAreSelected(
|
/**
|
||||||
data: Data,
|
* Reset the test state.
|
||||||
ids: string[],
|
*
|
||||||
strict = true
|
* ### Example
|
||||||
): boolean {
|
*
|
||||||
const selectedIds = tld.getSelectedIds(data)
|
*```ts
|
||||||
return (
|
* tt.reset()
|
||||||
(strict ? selectedIds.size === ids.length : true) &&
|
*```
|
||||||
ids.every((id) => selectedIds.has(id))
|
*/
|
||||||
)
|
reset(): TestState {
|
||||||
}
|
this.state.reset()
|
||||||
|
this.state
|
||||||
|
.send('MOUNTED')
|
||||||
|
.send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
|
||||||
|
|
||||||
export function hasParent(
|
return this
|
||||||
data: Data,
|
}
|
||||||
childId: string,
|
|
||||||
parentId: string
|
|
||||||
): boolean {
|
|
||||||
return tld.getShape(data, childId).parentId === parentId
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOnlySelectedShape(data: Data): Shape {
|
/**
|
||||||
const selectedShapes = tld.getSelectedShapes(data)
|
* Reset the document state. Will remove all shapes and extra pages.
|
||||||
return selectedShapes.length === 1 ? selectedShapes[0] : undefined
|
*
|
||||||
}
|
* ### Example
|
||||||
|
*
|
||||||
|
*```ts
|
||||||
|
* tt.resetDocumentState()
|
||||||
|
*```
|
||||||
|
*/
|
||||||
|
resetDocumentState(): TestState {
|
||||||
|
this.state.send('RESET_DOCUMENT_STATE')
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
export function assertShapeType(
|
/**
|
||||||
data: Data,
|
* Send a message to the state.
|
||||||
shapeId: string,
|
*
|
||||||
type: ShapeType
|
* ### Example
|
||||||
): boolean {
|
*
|
||||||
const shape = tld.getShape(data, shapeId)
|
*```ts
|
||||||
if (shape.type !== type) {
|
* tt.send("MOVED_TO_FRONT")
|
||||||
throw new TypeError(
|
*```
|
||||||
`expected shape ${shapeId} to be of type ${type}, found ${shape?.type} instead`
|
*/
|
||||||
|
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,
|
* Get whether the shape with the provided id has the provided parent id.
|
||||||
props: { [K in keyof Partial<T>]: T[K] }
|
*
|
||||||
): boolean {
|
* ### Example
|
||||||
for (const key in props) {
|
*
|
||||||
if (shape[key] !== props[key]) {
|
*```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(
|
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 }) {
|
function ErrorFallback({ error, resetErrorBoundary }) {
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.error(error)
|
|
||||||
const copy = 'Sorry, something went wrong. Clear canvas and continue?'
|
const copy = 'Sorry, something went wrong. Clear canvas and continue?'
|
||||||
|
console.error(error)
|
||||||
if (window.confirm(copy)) {
|
if (window.confirm(copy)) {
|
||||||
state.send('CLEARED_PAGE')
|
state.send('CLEARED_PAGE')
|
||||||
resetErrorBoundary()
|
resetErrorBoundary()
|
||||||
|
|
|
@ -6,23 +6,24 @@ export default function Presence(): JSX.Element {
|
||||||
const others = useCoopSelector((s) => s.data.others)
|
const others = useCoopSelector((s) => s.data.others)
|
||||||
const currentPageId = useSelector((s) => s.data.currentPageId)
|
const currentPageId = useSelector((s) => s.data.currentPageId)
|
||||||
|
|
||||||
|
if (!others) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{Object.values(others).map(({ connectionId, presence }) => {
|
{Object.values(others)
|
||||||
if (presence === null) return null
|
.filter(({ presence }) => presence?.pageId === currentPageId)
|
||||||
if (presence.pageId !== currentPageId) return null
|
.map(({ connectionId, presence }) => {
|
||||||
|
return (
|
||||||
return (
|
<Cursor
|
||||||
<Cursor
|
key={`cursor-${connectionId}`}
|
||||||
key={`cursor-${connectionId}`}
|
color={'red'}
|
||||||
color={'red'}
|
duration={presence.duration}
|
||||||
duration={presence.duration}
|
times={presence.times}
|
||||||
times={presence.times}
|
bufferedXs={presence.bufferedXs}
|
||||||
bufferedXs={presence.bufferedXs}
|
bufferedYs={presence.bufferedYs}
|
||||||
bufferedYs={presence.bufferedYs}
|
/>
|
||||||
/>
|
)
|
||||||
)
|
})}
|
||||||
})}
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,11 @@ import useShapeEvents from 'hooks/useShapeEvents'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { getShapeStyle } from 'state/shape-styles'
|
import { getShapeStyle } from 'state/shape-styles'
|
||||||
import useShapeDef from 'hooks/useShape'
|
import useShapeDef from 'hooks/useShape'
|
||||||
import { ShapeUtility } from 'types'
|
import { BooleanArraySupportOption } from 'prettier'
|
||||||
|
|
||||||
interface ShapeProps {
|
interface ShapeProps {
|
||||||
id: string
|
id: string
|
||||||
isSelecting: boolean
|
isSelecting: BooleanArraySupportOption
|
||||||
}
|
}
|
||||||
|
|
||||||
function Shape({ id, isSelecting }: ShapeProps): JSX.Element {
|
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
|
const isCurrentParent = useSelector((s) => {
|
||||||
// shape in state is a shape with changes that we need to render.
|
return s.data.currentParentId === id
|
||||||
|
})
|
||||||
|
|
||||||
|
const events = useShapeEvents(id, isCurrentParent, rGroup)
|
||||||
|
|
||||||
const shape = tld.getShape(state.data, id)
|
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
|
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 (
|
return (
|
||||||
<StyledGroup
|
<StyledGroup
|
||||||
id={id + '-group'}
|
id={id + '-group'}
|
||||||
ref={rGroup}
|
ref={rGroup}
|
||||||
transform={transform}
|
transform={transform}
|
||||||
|
isCurrentParent={isCurrentParent}
|
||||||
{...events}
|
{...events}
|
||||||
>
|
>
|
||||||
{isSelecting &&
|
{isSelecting &&
|
||||||
|
@ -204,4 +203,24 @@ const EventSoak = styled('use', {
|
||||||
|
|
||||||
const StyledGroup = styled('g', {
|
const StyledGroup = styled('g', {
|
||||||
outline: 'none',
|
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[]
|
size: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// type DeepPartial<T> = {
|
|
||||||
// [P in keyof T]?: DeepPartial<T[P]>
|
|
||||||
// }
|
|
||||||
|
|
||||||
type ShapeProps<T extends Shape> = {
|
type ShapeProps<T extends Shape> = {
|
||||||
[P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
|
[P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
|
||||||
}
|
}
|
||||||
|
|
||||||
type MutableShape =
|
interface MutableShapes {
|
||||||
| DotShape
|
[ShapeType.Dot]: DotShape
|
||||||
| EllipseShape
|
[ShapeType.Ellipse]: EllipseShape
|
||||||
| LineShape
|
[ShapeType.Line]: LineShape
|
||||||
| RayShape
|
[ShapeType.Ray]: RayShape
|
||||||
| PolylineShape
|
[ShapeType.Polyline]: PolylineShape
|
||||||
| DrawShape
|
[ShapeType.Draw]: DrawShape
|
||||||
| RectangleShape
|
[ShapeType.Rectangle]: RectangleShape
|
||||||
| ArrowShape
|
[ShapeType.Arrow]: ArrowShape
|
||||||
| TextShape
|
[ShapeType.Text]: TextShape
|
||||||
| GroupShape
|
[ShapeType.Group]: 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>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MutableShape = MutableShapes[keyof MutableShapes]
|
||||||
|
|
||||||
|
type Shapes = { [K in keyof MutableShapes]: Readonly<MutableShapes[K]> }
|
||||||
|
|
||||||
type Shape = Readonly<MutableShape>
|
type Shape = Readonly<MutableShape>
|
||||||
|
|
||||||
type ShapeByType<T extends ShapeType> = Shapes[T]
|
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 {
|
enum Decoration {
|
||||||
Arrow = 'Arrow',
|
Arrow = 'Arrow',
|
||||||
}
|
}
|
||||||
|
@ -232,6 +228,15 @@ interface PointerInfo {
|
||||||
altKey: boolean
|
altKey: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface KeyboardInfo {
|
||||||
|
key: string
|
||||||
|
keys: string[]
|
||||||
|
shiftKey: boolean
|
||||||
|
ctrlKey: boolean
|
||||||
|
metaKey: boolean
|
||||||
|
altKey: boolean
|
||||||
|
}
|
||||||
|
|
||||||
enum Edge {
|
enum Edge {
|
||||||
Top = 'top_edge',
|
Top = 'top_edge',
|
||||||
Right = 'right_edge',
|
Right = 'right_edge',
|
||||||
|
@ -276,8 +281,6 @@ interface BoundsSnapshot extends PointSnapshot {
|
||||||
nh: number
|
nh: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type Difference<A, B> = A extends B ? never : A
|
|
||||||
|
|
||||||
type ShapeSpecificProps<T extends Shape> = Pick<
|
type ShapeSpecificProps<T extends Shape> = Pick<
|
||||||
T,
|
T,
|
||||||
Difference<keyof T, keyof BaseShape>
|
Difference<keyof T, keyof BaseShape>
|
||||||
|
@ -561,6 +564,16 @@ interface ShapeUtility<K extends Shape> {
|
||||||
shouldRender(this: ShapeUtility<K>, shape: K, previous: K): boolean
|
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[]
|
size: number[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// type DeepPartial<T> = {
|
|
||||||
// [P in keyof T]?: DeepPartial<T[P]>
|
|
||||||
// }
|
|
||||||
|
|
||||||
type ShapeProps<T extends Shape> = {
|
type ShapeProps<T extends Shape> = {
|
||||||
[P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
|
[P in keyof T]?: P extends 'style' ? Partial<T[P]> : T[P]
|
||||||
}
|
}
|
||||||
|
|
||||||
type MutableShape =
|
interface MutableShapes {
|
||||||
| DotShape
|
[ShapeType.Dot]: DotShape
|
||||||
| EllipseShape
|
[ShapeType.Ellipse]: EllipseShape
|
||||||
| LineShape
|
[ShapeType.Line]: LineShape
|
||||||
| RayShape
|
[ShapeType.Ray]: RayShape
|
||||||
| PolylineShape
|
[ShapeType.Polyline]: PolylineShape
|
||||||
| DrawShape
|
[ShapeType.Draw]: DrawShape
|
||||||
| RectangleShape
|
[ShapeType.Rectangle]: RectangleShape
|
||||||
| ArrowShape
|
[ShapeType.Arrow]: ArrowShape
|
||||||
| TextShape
|
[ShapeType.Text]: TextShape
|
||||||
| GroupShape
|
[ShapeType.Group]: 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>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MutableShape = MutableShapes[keyof MutableShapes]
|
||||||
|
|
||||||
|
type Shapes = { [K in keyof MutableShapes]: Readonly<MutableShapes[K]> }
|
||||||
|
|
||||||
type Shape = Readonly<MutableShape>
|
type Shape = Readonly<MutableShape>
|
||||||
|
|
||||||
type ShapeByType<T extends ShapeType> = Shapes[T]
|
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 {
|
enum Decoration {
|
||||||
Arrow = 'Arrow',
|
Arrow = 'Arrow',
|
||||||
}
|
}
|
||||||
|
@ -782,6 +791,15 @@ interface PointerInfo {
|
||||||
altKey: boolean
|
altKey: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface KeyboardInfo {
|
||||||
|
key: string
|
||||||
|
keys: string[]
|
||||||
|
shiftKey: boolean
|
||||||
|
ctrlKey: boolean
|
||||||
|
metaKey: boolean
|
||||||
|
altKey: boolean
|
||||||
|
}
|
||||||
|
|
||||||
enum Edge {
|
enum Edge {
|
||||||
Top = 'top_edge',
|
Top = 'top_edge',
|
||||||
Right = 'right_edge',
|
Right = 'right_edge',
|
||||||
|
@ -826,8 +844,6 @@ interface BoundsSnapshot extends PointSnapshot {
|
||||||
nh: number
|
nh: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type Difference<A, B> = A extends B ? never : A
|
|
||||||
|
|
||||||
type ShapeSpecificProps<T extends Shape> = Pick<
|
type ShapeSpecificProps<T extends Shape> = Pick<
|
||||||
T,
|
T,
|
||||||
Difference<keyof T, keyof BaseShape>
|
Difference<keyof T, keyof BaseShape>
|
||||||
|
@ -1111,6 +1127,16 @@ interface ShapeUtility<K extends Shape> {
|
||||||
shouldRender(this: ShapeUtility<K>, shape: K, previous: K): boolean
|
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(() => {
|
fonts.load('12px Verveine Regular', 'Fonts are loaded!').then(() => {
|
||||||
state.send('MOUNTED')
|
state.send('MOUNTED')
|
||||||
|
|
||||||
if (roomId !== undefined) {
|
if (roomId !== undefined) {
|
||||||
state.send('RT_LOADED_ROOM', { id: roomId })
|
state.send('RT_LOADED_ROOM', { id: roomId })
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,15 @@ import Vec from 'utils/vec'
|
||||||
|
|
||||||
export default function useShapeEvents(
|
export default function useShapeEvents(
|
||||||
id: string,
|
id: string,
|
||||||
isParent: boolean,
|
isCurrentParent: boolean,
|
||||||
rGroup: MutableRefObject<SVGElement>
|
rGroup: MutableRefObject<SVGElement>
|
||||||
) {
|
) {
|
||||||
const handlePointerDown = useCallback(
|
const handlePointerDown = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
if (isParent) return
|
if (isCurrentParent) return
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
rGroup.current.setPointerCapture(e.pointerId)
|
rGroup.current.setPointerCapture(e.pointerId)
|
||||||
|
|
||||||
const info = inputs.pointerDown(e, id)
|
const info = inputs.pointerDown(e, id)
|
||||||
|
@ -28,30 +29,30 @@ export default function useShapeEvents(
|
||||||
state.send('RIGHT_POINTED', info)
|
state.send('RIGHT_POINTED', info)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[id]
|
[id, isCurrentParent]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerUp = useCallback(
|
const handlePointerUp = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
|
if (isCurrentParent) return
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
rGroup.current.releasePointerCapture(e.pointerId)
|
rGroup.current.releasePointerCapture(e.pointerId)
|
||||||
state.send('STOPPED_POINTING', inputs.pointerUp(e, id))
|
state.send('STOPPED_POINTING', inputs.pointerUp(e, id))
|
||||||
},
|
},
|
||||||
[id]
|
[id, isCurrentParent]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerEnter = useCallback(
|
const handlePointerEnter = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
|
if (isCurrentParent) return
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
if (isParent) {
|
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
||||||
state.send('HOVERED_GROUP', inputs.pointerEnter(e, id))
|
|
||||||
} else {
|
|
||||||
state.send('HOVERED_SHAPE', inputs.pointerEnter(e, id))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[id]
|
[id, isCurrentParent]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerMove = useCallback(
|
const handlePointerMove = useCallback(
|
||||||
|
@ -72,26 +73,22 @@ export default function useShapeEvents(
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isParent) {
|
if (isCurrentParent) return
|
||||||
state.send('MOVED_OVER_GROUP', inputs.pointerEnter(e, id))
|
|
||||||
} else {
|
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
||||||
state.send('MOVED_OVER_SHAPE', inputs.pointerEnter(e, id))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[id]
|
[id, isCurrentParent]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handlePointerLeave = useCallback(
|
const handlePointerLeave = useCallback(
|
||||||
(e: React.PointerEvent) => {
|
(e: React.PointerEvent) => {
|
||||||
|
if (isCurrentParent) return
|
||||||
if (!inputs.canAccept(e.pointerId)) return
|
if (!inputs.canAccept(e.pointerId)) return
|
||||||
|
e.stopPropagation()
|
||||||
|
|
||||||
if (isParent) {
|
state.send('UNHOVERED_SHAPE', { target: id })
|
||||||
state.send('UNHOVERED_GROUP', { target: id })
|
|
||||||
} else {
|
|
||||||
state.send('UNHOVERED_SHAPE', { target: id })
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
[id]
|
[id, isCurrentParent]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
const handleTouchStart = useCallback((e: React.TouchEvent) => {
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"test-all": "yarn lint && yarn type-check && yarn test",
|
"test-all": "yarn lint && yarn type-check && yarn test",
|
||||||
"test:update": "jest --updateSnapshot",
|
"test:update": "jest --updateSnapshot",
|
||||||
"test:watch": "jest --watchAll --verbose=false --silent=false",
|
"test:watch": "jest --watchAll",
|
||||||
"test": "jest --watchAll=false",
|
"test": "jest --watchAll=false",
|
||||||
"type-check": "tsc --pretty --noEmit"
|
"type-check": "tsc --pretty --noEmit"
|
||||||
},
|
},
|
||||||
|
@ -96,4 +96,4 @@
|
||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"useTabs": false
|
"useTabs": false
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { ArrowShape, Decoration, ShapeProps, ShapeType } from 'types'
|
import { ArrowShape, Decoration, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
TextCodeControl,
|
TextCodeControl,
|
||||||
VectorCodeControl,
|
VectorCodeControl,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
|
|
||||||
export const controls: Record<string, any> = {}
|
export const controls: Record<string, any> = {}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { DotShape, ShapeProps, ShapeType } from 'types'
|
import { DotShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { DrawShape, ShapeProps, ShapeType } from 'types'
|
import { DrawShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { EllipseShape, ShapeProps, ShapeType } from 'types'
|
import { EllipseShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { LineShape, ShapeProps, ShapeType } from 'types'
|
import { LineShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { PolylineShape, ShapeProps, ShapeType } from 'types'
|
import { PolylineShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { RayShape, ShapeProps, ShapeType } from 'types'
|
import { RayShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { RectangleShape, ShapeProps, ShapeType } from 'types'
|
import { RectangleShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import CodeShape from './index'
|
import CodeShape from './index'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { TextShape, ShapeProps, ShapeType } from 'types'
|
import { TextShape, ShapeProps, ShapeType } from 'types'
|
||||||
import { defaultStyle } from 'state/shape-styles'
|
import { defaultStyle } from 'state/shape-styles'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
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.savePage(data, data.document.id, fromPageId)
|
||||||
storage.loadPage(data, data.document.id, toPageId)
|
storage.loadPage(data, data.document.id, toPageId)
|
||||||
data.currentPageId = toPageId
|
data.currentPageId = toPageId
|
||||||
|
data.currentParentId = toPageId
|
||||||
},
|
},
|
||||||
undo(data) {
|
undo(data) {
|
||||||
storage.loadPage(data, data.document.id, fromPageId)
|
storage.loadPage(data, data.document.id, fromPageId)
|
||||||
data.currentPageId = fromPageId
|
data.currentPageId = fromPageId
|
||||||
|
data.currentParentId = fromPageId
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Command from './command'
|
import Command from './command'
|
||||||
import history from '../history'
|
import history from '../history'
|
||||||
import { Data, Page, PageState } from 'types'
|
import { Data, Page, PageState } from 'types'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import storage from 'state/storage'
|
import storage from 'state/storage'
|
||||||
|
|
||||||
export default function createPage(data: Data, goToPage = true): void {
|
export default function createPage(data: Data, goToPage = true): void {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import history from '../history'
|
||||||
import { Data } from 'types'
|
import { Data } from 'types'
|
||||||
import { deepClone } from 'utils'
|
import { deepClone } from 'utils'
|
||||||
import tld from 'utils/tld'
|
import tld from 'utils/tld'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
|
|
||||||
export default function duplicateCommand(data: Data): void {
|
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)
|
.sort((a, b) => b.childIndex - a.childIndex)
|
||||||
.forEach((shape) => moveForward(shape, siblings, visited))
|
.forEach((shape) => moveForward(shape, siblings, visited))
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case MoveType.Backward: {
|
case MoveType.Backward: {
|
||||||
|
|
|
@ -3,7 +3,7 @@ import history from '../history'
|
||||||
import { Data, Shape } from 'types'
|
import { Data, Shape } from 'types'
|
||||||
import { getCommonBounds, setToArray } from 'utils'
|
import { getCommonBounds, setToArray } from 'utils'
|
||||||
import tld from 'utils/tld'
|
import tld from 'utils/tld'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
import state from 'state/state'
|
import state from 'state/state'
|
||||||
|
|
|
@ -6,7 +6,7 @@ import {
|
||||||
MyPresenceCallback,
|
MyPresenceCallback,
|
||||||
OthersEventCallback,
|
OthersEventCallback,
|
||||||
} from '@liveblocks/client/lib/cjs/types'
|
} from '@liveblocks/client/lib/cjs/types'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
|
|
||||||
class CoopClient {
|
class CoopClient {
|
||||||
id = uniqueId()
|
id = uniqueId()
|
||||||
|
|
|
@ -160,6 +160,7 @@ class Inputs {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pointer = info
|
this.pointer = info
|
||||||
|
|
||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Data, GroupShape, Shape, ShapeType } from 'types'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import BaseSession from './base-session'
|
import BaseSession from './base-session'
|
||||||
import commands from 'state/commands'
|
import commands from 'state/commands'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { getShapeUtils } from 'state/shape-utils'
|
import { getShapeUtils } from 'state/shape-utils'
|
||||||
import tld from 'utils/tld'
|
import tld from 'utils/tld'
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,16 @@
|
||||||
import { getArcLength, uniqueId } from 'utils'
|
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import {
|
import {
|
||||||
|
getArcLength,
|
||||||
|
uniqueId,
|
||||||
getSvgPathFromStroke,
|
getSvgPathFromStroke,
|
||||||
rng,
|
rng,
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
translateBounds,
|
translateBounds,
|
||||||
pointInBounds,
|
pointInBounds,
|
||||||
pointInCircle,
|
pointInCircle,
|
||||||
|
circleFromThreePoints,
|
||||||
|
isAngleBetween,
|
||||||
|
getPerfectDashProps,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import {
|
import {
|
||||||
ArrowShape,
|
ArrowShape,
|
||||||
|
@ -15,7 +19,6 @@ import {
|
||||||
ShapeHandle,
|
ShapeHandle,
|
||||||
ShapeType,
|
ShapeType,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import { circleFromThreePoints, isAngleBetween } from 'utils'
|
|
||||||
import {
|
import {
|
||||||
intersectArcBounds,
|
intersectArcBounds,
|
||||||
intersectLineSegmentBounds,
|
intersectLineSegmentBounds,
|
||||||
|
@ -24,7 +27,6 @@ import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
import { getPerfectDashProps } from 'utils/dashes'
|
|
||||||
|
|
||||||
const pathCache = new WeakMap<ArrowShape, string>([])
|
const pathCache = new WeakMap<ArrowShape, string>([])
|
||||||
|
|
||||||
|
@ -37,55 +39,65 @@ function getCtp(shape: ArrowShape) {
|
||||||
const arrow = registerShapeUtils<ArrowShape>({
|
const arrow = registerShapeUtils<ArrowShape>({
|
||||||
boundsCache: new WeakMap([]),
|
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) {
|
create(props) {
|
||||||
const {
|
const shape = {
|
||||||
point = [0, 0],
|
...this.defaultProps,
|
||||||
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,
|
|
||||||
},
|
|
||||||
...props,
|
...props,
|
||||||
|
decorations: {
|
||||||
|
...this.defaultProps.decorations,
|
||||||
|
...props.decorations,
|
||||||
|
},
|
||||||
style: {
|
style: {
|
||||||
...defaultStyle,
|
...this.defaultProps.style,
|
||||||
...props.style,
|
...props.style,
|
||||||
isFilled: false,
|
isFilled: false,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shape.handles.bend.point = getBendPoint(shape)
|
||||||
|
|
||||||
|
return shape
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldRender(shape, prev) {
|
shouldRender(shape, prev) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import { DotShape, ShapeType } from 'types'
|
import { DotShape, ShapeType } from 'types'
|
||||||
import { intersectCircleBounds } from 'utils/intersections'
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
import { boundsContained, translateBounds } from 'utils'
|
import { boundsContained, translateBounds } from 'utils'
|
||||||
|
@ -8,27 +8,19 @@ import { registerShapeUtils } from './register'
|
||||||
const dot = registerShapeUtils<DotShape>({
|
const dot = registerShapeUtils<DotShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
type: ShapeType.Dot,
|
||||||
|
isGenerated: false,
|
||||||
type: ShapeType.Dot,
|
name: 'Dot',
|
||||||
isGenerated: false,
|
parentId: 'page1',
|
||||||
name: 'Dot',
|
childIndex: 0,
|
||||||
parentId: 'page1',
|
point: [0, 0],
|
||||||
childIndex: 0,
|
rotation: 0,
|
||||||
point: [0, 0],
|
isAspectRatioLocked: false,
|
||||||
rotation: 0,
|
isLocked: false,
|
||||||
isAspectRatioLocked: false,
|
isHidden: false,
|
||||||
isLocked: false,
|
style: defaultStyle,
|
||||||
isHidden: false,
|
|
||||||
...props,
|
|
||||||
style: {
|
|
||||||
...defaultStyle,
|
|
||||||
...props.style,
|
|
||||||
isFilled: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render({ id }) {
|
render({ id }) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
|
import { DashStyle, DrawShape, ShapeStyles, ShapeType } from 'types'
|
||||||
import { intersectPolylineBounds } from 'utils/intersections'
|
import { intersectPolylineBounds } from 'utils/intersections'
|
||||||
|
@ -22,27 +22,20 @@ const draw = registerShapeUtils<DrawShape>({
|
||||||
|
|
||||||
canStyleFill: true,
|
canStyleFill: true,
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
type: ShapeType.Draw,
|
||||||
|
isGenerated: false,
|
||||||
type: ShapeType.Draw,
|
name: 'Draw',
|
||||||
isGenerated: false,
|
parentId: 'page1',
|
||||||
name: 'Draw',
|
childIndex: 0,
|
||||||
parentId: 'page1',
|
point: [0, 0],
|
||||||
childIndex: 0,
|
points: [],
|
||||||
point: [0, 0],
|
rotation: 0,
|
||||||
points: [],
|
isAspectRatioLocked: false,
|
||||||
rotation: 0,
|
isLocked: false,
|
||||||
isAspectRatioLocked: false,
|
isHidden: false,
|
||||||
isLocked: false,
|
style: defaultStyle,
|
||||||
isHidden: false,
|
|
||||||
...props,
|
|
||||||
style: {
|
|
||||||
...defaultStyle,
|
|
||||||
...props.style,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldRender(shape, prev) {
|
shouldRender(shape, prev) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { getPerfectDashProps } from 'utils/dashes'
|
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { DashStyle, EllipseShape, ShapeType } from 'types'
|
import { DashStyle, EllipseShape, ShapeType } from 'types'
|
||||||
import { getShapeUtils } from './index'
|
import { getShapeUtils } from './index'
|
||||||
|
@ -11,6 +10,7 @@ import {
|
||||||
pointInEllipse,
|
pointInEllipse,
|
||||||
boundsContained,
|
boundsContained,
|
||||||
getRotatedEllipseBounds,
|
getRotatedEllipseBounds,
|
||||||
|
getPerfectDashProps,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
|
@ -21,25 +21,21 @@ const pathCache = new WeakMap<EllipseShape, string>([])
|
||||||
const ellipse = registerShapeUtils<EllipseShape>({
|
const ellipse = registerShapeUtils<EllipseShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
type: ShapeType.Ellipse,
|
||||||
|
isGenerated: false,
|
||||||
type: ShapeType.Ellipse,
|
name: 'Ellipse',
|
||||||
isGenerated: false,
|
parentId: 'page1',
|
||||||
name: 'Ellipse',
|
childIndex: 0,
|
||||||
parentId: 'page1',
|
point: [0, 0],
|
||||||
childIndex: 0,
|
radiusX: 1,
|
||||||
point: [0, 0],
|
radiusY: 1,
|
||||||
radiusX: 1,
|
rotation: 0,
|
||||||
radiusY: 1,
|
isAspectRatioLocked: false,
|
||||||
rotation: 0,
|
isLocked: false,
|
||||||
isAspectRatioLocked: false,
|
isHidden: false,
|
||||||
isLocked: false,
|
style: defaultStyle,
|
||||||
isHidden: false,
|
|
||||||
style: defaultStyle,
|
|
||||||
...props,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldRender(shape, prev) {
|
shouldRender(shape, prev) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { GroupShape, ShapeType } from 'types'
|
import { GroupShape, ShapeType } from 'types'
|
||||||
import { getShapeUtils } from './index'
|
import { getShapeUtils } from './index'
|
||||||
|
@ -12,26 +12,21 @@ const group = registerShapeUtils<GroupShape>({
|
||||||
isShy: true,
|
isShy: true,
|
||||||
isParent: true,
|
isParent: true,
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
type: ShapeType.Group,
|
||||||
|
isGenerated: false,
|
||||||
type: ShapeType.Group,
|
name: 'Group',
|
||||||
isGenerated: false,
|
parentId: 'page1',
|
||||||
name: 'Group',
|
childIndex: 0,
|
||||||
parentId: 'page1',
|
point: [0, 0],
|
||||||
childIndex: 0,
|
size: [1, 1],
|
||||||
point: [0, 0],
|
rotation: 0,
|
||||||
size: [1, 1],
|
isAspectRatioLocked: false,
|
||||||
radius: 2,
|
isLocked: false,
|
||||||
rotation: 0,
|
isHidden: false,
|
||||||
isAspectRatioLocked: false,
|
style: defaultStyle,
|
||||||
isLocked: false,
|
children: [],
|
||||||
isHidden: false,
|
|
||||||
style: defaultStyle,
|
|
||||||
children: [],
|
|
||||||
...props,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
render(shape) {
|
render(shape) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { LineShape, ShapeType } from 'types'
|
import { LineShape, ShapeType } from 'types'
|
||||||
import { intersectCircleBounds } from 'utils/intersections'
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
|
@ -10,28 +10,20 @@ import { registerShapeUtils } from './register'
|
||||||
const line = registerShapeUtils<LineShape>({
|
const line = registerShapeUtils<LineShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
type: ShapeType.Line,
|
||||||
|
isGenerated: false,
|
||||||
type: ShapeType.Line,
|
name: 'Line',
|
||||||
isGenerated: false,
|
parentId: 'page1',
|
||||||
name: 'Line',
|
childIndex: 0,
|
||||||
parentId: 'page1',
|
point: [0, 0],
|
||||||
childIndex: 0,
|
direction: [0, 0],
|
||||||
point: [0, 0],
|
rotation: 0,
|
||||||
direction: [0, 0],
|
isAspectRatioLocked: false,
|
||||||
rotation: 0,
|
isLocked: false,
|
||||||
isAspectRatioLocked: false,
|
isHidden: false,
|
||||||
isLocked: false,
|
style: defaultStyle,
|
||||||
isHidden: false,
|
|
||||||
...props,
|
|
||||||
style: {
|
|
||||||
...defaultStyle,
|
|
||||||
...props.style,
|
|
||||||
isFilled: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldRender(shape, prev) {
|
shouldRender(shape, prev) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { PolylineShape, ShapeType } from 'types'
|
import { PolylineShape, ShapeType } from 'types'
|
||||||
import { intersectPolylineBounds } from 'utils/intersections'
|
import { intersectPolylineBounds } from 'utils/intersections'
|
||||||
|
@ -13,24 +13,20 @@ import { registerShapeUtils } from './register'
|
||||||
const polyline = registerShapeUtils<PolylineShape>({
|
const polyline = registerShapeUtils<PolylineShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
type: ShapeType.Polyline,
|
||||||
|
isGenerated: false,
|
||||||
type: ShapeType.Polyline,
|
name: 'Polyline',
|
||||||
isGenerated: false,
|
parentId: 'page1',
|
||||||
name: 'Polyline',
|
childIndex: 0,
|
||||||
parentId: 'page1',
|
point: [0, 0],
|
||||||
childIndex: 0,
|
points: [[0, 0]],
|
||||||
point: [0, 0],
|
rotation: 0,
|
||||||
points: [[0, 0]],
|
isAspectRatioLocked: false,
|
||||||
rotation: 0,
|
isLocked: false,
|
||||||
isAspectRatioLocked: false,
|
isHidden: false,
|
||||||
isLocked: false,
|
style: defaultStyle,
|
||||||
isHidden: false,
|
|
||||||
style: defaultStyle,
|
|
||||||
...props,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldRender(shape, prev) {
|
shouldRender(shape, prev) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { RayShape, ShapeType } from 'types'
|
import { RayShape, ShapeType } from 'types'
|
||||||
import { intersectCircleBounds } from 'utils/intersections'
|
import { intersectCircleBounds } from 'utils/intersections'
|
||||||
|
@ -10,28 +10,20 @@ import { registerShapeUtils } from './register'
|
||||||
const ray = registerShapeUtils<RayShape>({
|
const ray = registerShapeUtils<RayShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
type: ShapeType.Ray,
|
||||||
|
isGenerated: false,
|
||||||
type: ShapeType.Ray,
|
name: 'Ray',
|
||||||
isGenerated: false,
|
parentId: 'page1',
|
||||||
name: 'Ray',
|
childIndex: 0,
|
||||||
parentId: 'page1',
|
point: [0, 0],
|
||||||
childIndex: 0,
|
direction: [0, 1],
|
||||||
point: [0, 0],
|
rotation: 0,
|
||||||
direction: [0, 1],
|
isAspectRatioLocked: false,
|
||||||
rotation: 0,
|
isLocked: false,
|
||||||
isAspectRatioLocked: false,
|
isHidden: false,
|
||||||
isLocked: false,
|
style: defaultStyle,
|
||||||
isHidden: false,
|
|
||||||
...props,
|
|
||||||
style: {
|
|
||||||
...defaultStyle,
|
|
||||||
...props.style,
|
|
||||||
isFilled: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldRender(shape, prev) {
|
shouldRender(shape, prev) {
|
||||||
|
|
|
@ -1,36 +1,32 @@
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId, getPerfectDashProps } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { DashStyle, RectangleShape, ShapeType } from 'types'
|
import { DashStyle, RectangleShape, ShapeType } from 'types'
|
||||||
import { getSvgPathFromStroke, translateBounds, rng, shuffleArr } from 'utils'
|
import { getSvgPathFromStroke, translateBounds, rng, shuffleArr } from 'utils'
|
||||||
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
import { defaultStyle, getShapeStyle } from 'state/shape-styles'
|
||||||
import getStroke from 'perfect-freehand'
|
import getStroke from 'perfect-freehand'
|
||||||
import { registerShapeUtils } from './register'
|
import { registerShapeUtils } from './register'
|
||||||
import { getPerfectDashProps } from 'utils/dashes'
|
|
||||||
|
|
||||||
const pathCache = new WeakMap<number[], string>([])
|
const pathCache = new WeakMap<number[], string>([])
|
||||||
|
|
||||||
const rectangle = registerShapeUtils<RectangleShape>({
|
const rectangle = registerShapeUtils<RectangleShape>({
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
|
||||||
|
|
||||||
type: ShapeType.Rectangle,
|
type: ShapeType.Rectangle,
|
||||||
isGenerated: false,
|
isGenerated: false,
|
||||||
name: 'Rectangle',
|
name: 'Rectangle',
|
||||||
parentId: 'page1',
|
parentId: 'page1',
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
point: [0, 0],
|
point: [0, 0],
|
||||||
size: [1, 1],
|
size: [1, 1],
|
||||||
radius: 2,
|
radius: 2,
|
||||||
rotation: 0,
|
rotation: 0,
|
||||||
isAspectRatioLocked: false,
|
isAspectRatioLocked: false,
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
style: defaultStyle,
|
style: defaultStyle,
|
||||||
...props,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldRender(shape, prev) {
|
shouldRender(shape, prev) {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Shape, ShapeUtility } from 'types'
|
import React from 'react'
|
||||||
import vec from 'utils/vec'
|
|
||||||
import {
|
import {
|
||||||
|
vec,
|
||||||
pointInBounds,
|
pointInBounds,
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
getBoundsFromPoints,
|
getBoundsFromPoints,
|
||||||
|
@ -8,8 +8,7 @@ import {
|
||||||
boundsCollidePolygon,
|
boundsCollidePolygon,
|
||||||
boundsContainPolygon,
|
boundsContainPolygon,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import { uniqueId } from 'utils'
|
import { Shape, ShapeUtility } from 'types'
|
||||||
import React from 'react'
|
|
||||||
|
|
||||||
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
return {
|
return {
|
||||||
|
@ -22,19 +21,19 @@ function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
|
||||||
isParent: false,
|
isParent: false,
|
||||||
isForeignObject: false,
|
isForeignObject: false,
|
||||||
|
|
||||||
|
defaultProps: {} as T,
|
||||||
|
|
||||||
create(props) {
|
create(props) {
|
||||||
return {
|
return {
|
||||||
id: uniqueId(),
|
...this.defaultProps,
|
||||||
isGenerated: false,
|
|
||||||
point: [0, 0],
|
|
||||||
name: 'Shape',
|
|
||||||
parentId: 'page1',
|
|
||||||
childIndex: 0,
|
|
||||||
rotation: 0,
|
|
||||||
isAspectRatioLocked: false,
|
|
||||||
isLocked: false,
|
|
||||||
isHidden: false,
|
|
||||||
...props,
|
...props,
|
||||||
|
style: {
|
||||||
|
...this.defaultProps.style,
|
||||||
|
...props.style,
|
||||||
|
isFilled: this.canStyleFill
|
||||||
|
? props.style?.isFilled || this.defaultProps.style.isFilled
|
||||||
|
: false,
|
||||||
|
},
|
||||||
} as T
|
} as T
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { uniqueId, isMobile } from 'utils'
|
import { uniqueId, isMobile } from 'utils/utils'
|
||||||
import vec from 'utils/vec'
|
import vec from 'utils/vec'
|
||||||
import { TextShape, ShapeType } from 'types'
|
import { TextShape, ShapeType } from 'types'
|
||||||
import {
|
import {
|
||||||
|
@ -47,27 +47,23 @@ const text = registerShapeUtils<TextShape>({
|
||||||
isForeignObject: true,
|
isForeignObject: true,
|
||||||
canChangeAspectRatio: false,
|
canChangeAspectRatio: false,
|
||||||
canEdit: true,
|
canEdit: true,
|
||||||
|
|
||||||
boundsCache: new WeakMap([]),
|
boundsCache: new WeakMap([]),
|
||||||
|
|
||||||
create(props) {
|
defaultProps: {
|
||||||
return {
|
id: uniqueId(),
|
||||||
id: uniqueId(),
|
type: ShapeType.Text,
|
||||||
type: ShapeType.Text,
|
isGenerated: false,
|
||||||
isGenerated: false,
|
name: 'Text',
|
||||||
name: 'Text',
|
parentId: 'page1',
|
||||||
parentId: 'page1',
|
childIndex: 0,
|
||||||
childIndex: 0,
|
point: [0, 0],
|
||||||
point: [0, 0],
|
rotation: 0,
|
||||||
rotation: 0,
|
isAspectRatioLocked: false,
|
||||||
isAspectRatioLocked: false,
|
isLocked: false,
|
||||||
isLocked: false,
|
isHidden: false,
|
||||||
isHidden: false,
|
style: defaultStyle,
|
||||||
style: defaultStyle,
|
text: '',
|
||||||
text: '',
|
scale: 1,
|
||||||
scale: 1,
|
|
||||||
...props,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
shouldRender(shape, prev) {
|
shouldRender(shape, prev) {
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { createSelectorHook, createState } from '@state-designer/react'
|
import { createSelectorHook, createState } from '@state-designer/react'
|
||||||
import { updateFromCode } from './code/generate'
|
import { updateFromCode } from './code/generate'
|
||||||
import { createShape, getShapeUtils } from './shape-utils'
|
import { createShape, getShapeUtils } from './shape-utils'
|
||||||
import vec from 'utils/vec'
|
import * as Sessions from './sessions'
|
||||||
import inputs from './inputs'
|
import inputs from './inputs'
|
||||||
import history from './history'
|
import history from './history'
|
||||||
import storage from './storage'
|
import storage from './storage'
|
||||||
|
import session from './session'
|
||||||
import clipboard from './clipboard'
|
import clipboard from './clipboard'
|
||||||
import * as Sessions from './sessions'
|
|
||||||
import coopClient from './coop/client-liveblocks'
|
import coopClient from './coop/client-liveblocks'
|
||||||
import commands from './commands'
|
import commands from './commands'
|
||||||
import {
|
import {
|
||||||
|
vec,
|
||||||
getCommonBounds,
|
getCommonBounds,
|
||||||
rotateBounds,
|
rotateBounds,
|
||||||
getBoundsCenter,
|
getBoundsCenter,
|
||||||
|
@ -18,7 +19,7 @@ import {
|
||||||
pointInBounds,
|
pointInBounds,
|
||||||
uniqueId,
|
uniqueId,
|
||||||
} from 'utils'
|
} from 'utils'
|
||||||
import tld from 'utils/tld'
|
import tld from '../utils/tld'
|
||||||
import {
|
import {
|
||||||
Data,
|
Data,
|
||||||
PointerInfo,
|
PointerInfo,
|
||||||
|
@ -36,7 +37,6 @@ import {
|
||||||
SizeStyle,
|
SizeStyle,
|
||||||
ColorStyle,
|
ColorStyle,
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import session from './session'
|
|
||||||
|
|
||||||
const initialData: Data = {
|
const initialData: Data = {
|
||||||
isReadOnly: false,
|
isReadOnly: false,
|
||||||
|
@ -288,10 +288,15 @@ const state = createState({
|
||||||
unless: 'isInSession',
|
unless: 'isInSession',
|
||||||
do: ['loadDocumentFromJson', 'resetHistory'],
|
do: ['loadDocumentFromJson', 'resetHistory'],
|
||||||
},
|
},
|
||||||
|
DESELECTED_ALL: {
|
||||||
|
unless: 'isInSession',
|
||||||
|
do: 'deselectAll',
|
||||||
|
to: 'selecting',
|
||||||
|
},
|
||||||
SELECTED_ALL: {
|
SELECTED_ALL: {
|
||||||
unless: 'isInSession',
|
unless: 'isInSession',
|
||||||
to: 'selecting',
|
|
||||||
do: 'selectAll',
|
do: 'selectAll',
|
||||||
|
to: 'selecting',
|
||||||
},
|
},
|
||||||
CHANGED_PAGE: {
|
CHANGED_PAGE: {
|
||||||
unless: 'isInSession',
|
unless: 'isInSession',
|
||||||
|
@ -398,8 +403,15 @@ const state = createState({
|
||||||
notPointing: {
|
notPointing: {
|
||||||
onEnter: 'clearPointedId',
|
onEnter: 'clearPointedId',
|
||||||
on: {
|
on: {
|
||||||
CANCELLED: 'clearSelectedIds',
|
CANCELLED: {
|
||||||
POINTED_CANVAS: { to: 'brushSelecting' },
|
if: 'hasCurrentParentShape',
|
||||||
|
do: ['selectCurrentParentId', 'raiseCurrentParentId'],
|
||||||
|
else: 'clearSelectedIds',
|
||||||
|
},
|
||||||
|
POINTED_CANVAS: {
|
||||||
|
to: 'brushSelecting',
|
||||||
|
do: 'setCurrentParentIdToPage',
|
||||||
|
},
|
||||||
POINTED_BOUNDS: [
|
POINTED_BOUNDS: [
|
||||||
{
|
{
|
||||||
if: 'isPressingMetaKey',
|
if: 'isPressingMetaKey',
|
||||||
|
@ -477,7 +489,7 @@ const state = createState({
|
||||||
{
|
{
|
||||||
unless: 'isPressingShiftKey',
|
unless: 'isPressingShiftKey',
|
||||||
do: [
|
do: [
|
||||||
'setDrilledPointedId',
|
'setCurrentParentId',
|
||||||
'clearSelectedIds',
|
'clearSelectedIds',
|
||||||
'pushPointedIdToSelectedIds',
|
'pushPointedIdToSelectedIds',
|
||||||
],
|
],
|
||||||
|
@ -1120,6 +1132,9 @@ const state = createState({
|
||||||
hasMultipleSelection(data) {
|
hasMultipleSelection(data) {
|
||||||
return tld.getSelectedIds(data).size > 1
|
return tld.getSelectedIds(data).size > 1
|
||||||
},
|
},
|
||||||
|
hasCurrentParentShape(data) {
|
||||||
|
return data.currentParentId !== data.currentPageId
|
||||||
|
},
|
||||||
isToolLocked(data) {
|
isToolLocked(data) {
|
||||||
return data.settings.isToolLocked
|
return data.settings.isToolLocked
|
||||||
},
|
},
|
||||||
|
@ -1180,6 +1195,14 @@ const state = createState({
|
||||||
|
|
||||||
data.currentPageId = newId
|
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 = {
|
data.document.pages = {
|
||||||
[newId]: {
|
[newId]: {
|
||||||
id: newId,
|
id: newId,
|
||||||
|
@ -1234,6 +1257,7 @@ const state = createState({
|
||||||
|
|
||||||
createShape(data, payload, type: ShapeType) {
|
createShape(data, payload, type: ShapeType) {
|
||||||
const shape = createShape(type, {
|
const shape = createShape(type, {
|
||||||
|
id: uniqueId(),
|
||||||
parentId: data.currentPageId,
|
parentId: data.currentPageId,
|
||||||
point: vec.round(tld.screenToWorld(payload.point, data)),
|
point: vec.round(tld.screenToWorld(payload.point, data)),
|
||||||
style: deepClone(data.currentStyle),
|
style: deepClone(data.currentStyle),
|
||||||
|
@ -1500,11 +1524,12 @@ const state = createState({
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
clearInputs() {
|
clearInputs() {
|
||||||
inputs.clear()
|
inputs.clear()
|
||||||
},
|
},
|
||||||
|
deselectAll(data) {
|
||||||
|
tld.getSelectedIds(data).clear()
|
||||||
|
},
|
||||||
selectAll(data) {
|
selectAll(data) {
|
||||||
const selectedIds = tld.getSelectedIds(data)
|
const selectedIds = tld.getSelectedIds(data)
|
||||||
const page = tld.getPage(data)
|
const page = tld.getPage(data)
|
||||||
|
@ -1525,10 +1550,24 @@ const state = createState({
|
||||||
data.pointedId = getPointedId(data, payload.target)
|
data.pointedId = getPointedId(data, payload.target)
|
||||||
data.currentParentId = getParentId(data, data.pointedId)
|
data.currentParentId = getParentId(data, data.pointedId)
|
||||||
},
|
},
|
||||||
setDrilledPointedId(data, payload: PointerInfo) {
|
setCurrentParentId(data, payload: PointerInfo) {
|
||||||
data.pointedId = getDrilledPointedId(data, payload.target)
|
data.pointedId = getDrilledPointedId(data, payload.target)
|
||||||
data.currentParentId = getParentId(data, data.pointedId)
|
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) {
|
clearCurrentParentId(data) {
|
||||||
data.currentParentId = data.currentPageId
|
data.currentParentId = data.currentPageId
|
||||||
data.pointedId = undefined
|
data.pointedId = undefined
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { Data, PageState, TLDocument } from 'types'
|
import { Data, PageState, TLDocument } from 'types'
|
||||||
import { decompress, compress, setToArray } from 'utils'
|
import { decompress, compress, setToArray } from 'utils'
|
||||||
import state from './state'
|
import state from './state'
|
||||||
import { uniqueId } from 'utils'
|
import { uniqueId } from 'utils/utils'
|
||||||
import * as idb from 'idb-keyval'
|
import * as idb from 'idb-keyval'
|
||||||
|
|
||||||
const CURRENT_VERSION = 'code_slate_0.0.8'
|
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 type Mutable<T extends Shape> = { -readonly [K in keyof T]: T[K] }
|
||||||
|
|
||||||
export interface ShapeUtility<K extends Shape> {
|
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.
|
// A cache for the computed bounds of this kind of shape.
|
||||||
boundsCache: WeakMap<K, Bounds>
|
boundsCache: WeakMap<K, Bounds>
|
||||||
|
|
||||||
|
@ -483,7 +486,7 @@ export interface ShapeUtility<K extends Shape> {
|
||||||
isShy: boolean
|
isShy: boolean
|
||||||
|
|
||||||
// Create a new shape.
|
// Create a new shape.
|
||||||
create(props: Partial<K>): K
|
create(this: ShapeUtility<K>, props: Partial<K>): K
|
||||||
|
|
||||||
// Update a shape's styles
|
// Update a shape's styles
|
||||||
applyStyles(
|
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 * 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 { getShapeUtils } from 'state/shape-utils'
|
||||||
import vec from './vec'
|
import vec from './vec'
|
||||||
import {
|
import {
|
||||||
|
@ -15,7 +15,7 @@ import {
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import { AssertionError } from 'assert'
|
import { AssertionError } from 'assert'
|
||||||
|
|
||||||
export default class ProjectUtils {
|
export default class StateUtils {
|
||||||
static getCameraZoom(zoom: number): number {
|
static getCameraZoom(zoom: number): number {
|
||||||
return clamp(zoom, 0.1, 5)
|
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…
Add table
Add a link
Reference in a new issue