Merge pull request #31 from tldraw/testing-30-transform-command

Adds testMode, tests for transform
This commit is contained in:
Steve Ruiz 2021-07-09 10:34:14 +01:00 committed by GitHub
commit b2904d6a1e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1796 additions and 231 deletions

View file

@ -12,3 +12,5 @@ jobs:
uses: mattallty/jest-github-action@v1.0.3 uses: mattallty/jest-github-action@v1.0.3
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
test-command: 'yarn test'

View file

@ -15,8 +15,8 @@
"name": "Rectangle", "name": "Rectangle",
"parentId": "page1", "parentId": "page1",
"childIndex": 3, "childIndex": 3,
"point": [171.47, 288.63], "point": [100, 100],
"size": [176.22, 192.26], "size": [100, 100],
"radius": 2, "radius": 2,
"rotation": 0, "rotation": 0,
"style": { "style": {
@ -32,8 +32,8 @@
"name": "Rectangle", "name": "Rectangle",
"parentId": "page1", "parentId": "page1",
"childIndex": 4, "childIndex": 4,
"point": [511.7, 404.19], "point": [500, 400],
"size": [181.08999999999992, 150.40999999999997], "size": [200, 200],
"radius": 2, "radius": 2,
"rotation": 0, "rotation": 0,
"style": { "style": {

View file

@ -64,14 +64,14 @@ for (let i = 0; i < count; i++) {
"name": "Rectangle", "name": "Rectangle",
"parentId": "page1", "parentId": "page1",
"point": Array [ "point": Array [
511.7, 500,
404.19, 400,
], ],
"radius": 2, "radius": 2,
"rotation": 0, "rotation": 0,
"size": Array [ "size": Array [
181.08999999999992, 200,
150.40999999999997, 200,
], ],
"style": Object { "style": Object {
"color": "Black", "color": "Black",
@ -330,14 +330,14 @@ for (let i = 0; i < count; i++) {
"name": "Rectangle", "name": "Rectangle",
"parentId": "page1", "parentId": "page1",
"point": Array [ "point": Array [
171.47, 100,
288.63, 100,
], ],
"radius": 2, "radius": 2,
"rotation": 0, "rotation": 0,
"size": Array [ "size": Array [
176.22, 100,
192.26, 100,
], ],
"style": Object { "style": Object {
"color": "Black", "color": "Black",
@ -468,14 +468,14 @@ for (let i = 0; i < count; i++) {
"name": "Rectangle", "name": "Rectangle",
"parentId": "page1", "parentId": "page1",
"point": Array [ "point": Array [
511.7, 500,
404.19, 400,
], ],
"radius": 2, "radius": 2,
"rotation": 0, "rotation": 0,
"size": Array [ "size": Array [
181.08999999999992, 200,
150.40999999999997, 200,
], ],
"style": Object { "style": Object {
"color": "Black", "color": "Black",
@ -734,14 +734,14 @@ for (let i = 0; i < count; i++) {
"name": "Rectangle", "name": "Rectangle",
"parentId": "page1", "parentId": "page1",
"point": Array [ "point": Array [
171.47, 100,
288.63, 100,
], ],
"radius": 2, "radius": 2,
"rotation": 0, "rotation": 0,
"size": Array [ "size": Array [
176.22, 100,
192.26, 100,
], ],
"style": Object { "style": Object {
"color": "Black", "color": "Black",

View file

@ -0,0 +1,852 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transform command snapshot tests shift-transforms corners 1`] = `
Object {
"height": 593.73,
"maxX": 700,
"maxY": 600,
"minX": -12.476,
"minY": 6.27,
"width": 712.476,
}
`;
exports[`transform command snapshot tests shift-transforms corners 2`] = `
Object {
"rect1": Object {
"point": Array [
-12.476,
6.27,
],
"size": Array [
118.75,
118.75,
],
},
"rect2": Object {
"point": Array [
462.51,
362.51,
],
"size": Array [
237.49,
237.49,
],
},
}
`;
exports[`transform command snapshot tests shift-transforms corners 3`] = `
Object {
"height": 531.6320000000001,
"maxX": 737.9599999999999,
"maxY": 600,
"minX": 100,
"minY": 68.368,
"width": 637.9599999999999,
}
`;
exports[`transform command snapshot tests shift-transforms corners 4`] = `
Object {
"rect1": Object {
"point": Array [
100,
68.368,
],
"size": Array [
106.33,
106.33,
],
},
"rect2": Object {
"point": Array [
525.31,
387.35,
],
"size": Array [
212.65,
212.65,
],
},
}
`;
exports[`transform command snapshot tests shift-transforms corners 5`] = `
Object {
"height": 533.62,
"maxX": 740.3499999999999,
"maxY": 633.62,
"minX": 100,
"minY": 100,
"width": 640.3499999999999,
}
`;
exports[`transform command snapshot tests shift-transforms corners 6`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
106.72,
106.72,
],
},
"rect2": Object {
"point": Array [
526.9,
420.17,
],
"size": Array [
213.45,
213.45,
],
},
}
`;
exports[`transform command snapshot tests shift-transforms corners 7`] = `
Object {
"height": 490.52,
"maxX": 700,
"maxY": 590.52,
"minX": 111.38,
"minY": 100,
"width": 588.62,
}
`;
exports[`transform command snapshot tests shift-transforms corners 8`] = `
Object {
"rect1": Object {
"point": Array [
111.38,
100,
],
"size": Array [
98.104,
98.104,
],
},
"rect2": Object {
"point": Array [
503.79,
394.31,
],
"size": Array [
196.21,
196.21,
],
},
}
`;
exports[`transform command snapshot tests shift-transforms edges 1`] = `
Object {
"height": 593.73,
"maxX": 756.24,
"maxY": 600,
"minX": 43.762,
"minY": 6.27,
"width": 712.4780000000001,
}
`;
exports[`transform command snapshot tests shift-transforms edges 2`] = `
Object {
"rect1": Object {
"point": Array [
43.762,
6.27,
],
"size": Array [
118.75,
118.75,
],
},
"rect2": Object {
"point": Array [
518.75,
362.51,
],
"size": Array [
237.49,
237.49,
],
},
}
`;
exports[`transform command snapshot tests shift-transforms edges 3`] = `
Object {
"height": 531.6260000000001,
"maxX": 737.9599999999999,
"maxY": 615.8100000000001,
"minX": 100,
"minY": 84.184,
"width": 637.9599999999999,
}
`;
exports[`transform command snapshot tests shift-transforms edges 4`] = `
Object {
"rect1": Object {
"point": Array [
100,
84.184,
],
"size": Array [
106.33,
106.33,
],
},
"rect2": Object {
"point": Array [
525.31,
403.16,
],
"size": Array [
212.65,
212.65,
],
},
}
`;
exports[`transform command snapshot tests shift-transforms edges 5`] = `
Object {
"height": 411.35,
"maxX": 646.81,
"maxY": 511.35,
"minX": 153.19,
"minY": 100,
"width": 493.61999999999995,
}
`;
exports[`transform command snapshot tests shift-transforms edges 6`] = `
Object {
"rect1": Object {
"point": Array [
153.19,
100,
],
"size": Array [
82.269,
82.269,
],
},
"rect2": Object {
"point": Array [
482.27,
346.81,
],
"size": Array [
164.54,
164.54,
],
},
}
`;
exports[`transform command snapshot tests shift-transforms edges 7`] = `
Object {
"height": 490.52,
"maxX": 700,
"maxY": 595.26,
"minX": 111.38,
"minY": 104.74,
"width": 588.62,
}
`;
exports[`transform command snapshot tests shift-transforms edges 8`] = `
Object {
"rect1": Object {
"point": Array [
111.38,
104.74,
],
"size": Array [
98.104,
98.104,
],
},
"rect2": Object {
"point": Array [
503.79,
399.05,
],
"size": Array [
196.21,
196.21,
],
},
}
`;
exports[`transform command snapshot tests transforms corners 1`] = `
Object {
"height": 593.73,
"maxX": 700,
"maxY": 600,
"minX": 27.892,
"minY": 6.27,
"width": 672.108,
}
`;
exports[`transform command snapshot tests transforms corners 2`] = `
Object {
"rect1": Object {
"point": Array [
27.892,
6.27,
],
"size": Array [
112.02,
118.75,
],
},
"rect2": Object {
"point": Array [
475.96,
362.51,
],
"size": Array [
224.04,
237.49,
],
},
}
`;
exports[`transform command snapshot tests transforms corners 3`] = `
Object {
"height": 490.17,
"maxX": 737.9599999999999,
"maxY": 600,
"minX": 100,
"minY": 109.83,
"width": 637.9599999999999,
}
`;
exports[`transform command snapshot tests transforms corners 4`] = `
Object {
"rect1": Object {
"point": Array [
100,
109.83,
],
"size": Array [
106.33,
98.034,
],
},
"rect2": Object {
"point": Array [
525.31,
403.93,
],
"size": Array [
212.65,
196.07,
],
},
}
`;
exports[`transform command snapshot tests transforms corners 5`] = `
Object {
"height": 411.35,
"maxX": 740.3499999999999,
"maxY": 511.35,
"minX": 100,
"minY": 100,
"width": 640.3499999999999,
}
`;
exports[`transform command snapshot tests transforms corners 6`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
106.72,
82.269,
],
},
"rect2": Object {
"point": Array [
526.9,
346.81,
],
"size": Array [
213.45,
164.54,
],
},
}
`;
exports[`transform command snapshot tests transforms corners 7`] = `
Object {
"height": 437.4,
"maxX": 700,
"maxY": 537.4,
"minX": 111.38,
"minY": 100,
"width": 588.62,
}
`;
exports[`transform command snapshot tests transforms corners 8`] = `
Object {
"rect1": Object {
"point": Array [
111.38,
100,
],
"size": Array [
98.104,
87.479,
],
},
"rect2": Object {
"point": Array [
503.79,
362.44,
],
"size": Array [
196.21,
174.96,
],
},
}
`;
exports[`transform command snapshot tests transforms edges 1`] = `
Object {
"height": 593.73,
"maxX": 700,
"maxY": 600,
"minX": 100,
"minY": 6.27,
"width": 600,
}
`;
exports[`transform command snapshot tests transforms edges 2`] = `
Object {
"rect1": Object {
"point": Array [
100,
6.27,
],
"size": Array [
100,
118.75,
],
},
"rect2": Object {
"point": Array [
500,
362.51,
],
"size": Array [
200,
237.49,
],
},
}
`;
exports[`transform command snapshot tests transforms edges 3`] = `
Object {
"height": 500,
"maxX": 737.9599999999999,
"maxY": 600,
"minX": 100,
"minY": 100,
"width": 637.9599999999999,
}
`;
exports[`transform command snapshot tests transforms edges 4`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
106.33,
100,
],
},
"rect2": Object {
"point": Array [
525.31,
400,
],
"size": Array [
212.65,
200,
],
},
}
`;
exports[`transform command snapshot tests transforms edges 5`] = `
Object {
"height": 411.35,
"maxX": 700,
"maxY": 511.35,
"minX": 100,
"minY": 100,
"width": 600,
}
`;
exports[`transform command snapshot tests transforms edges 6`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
100,
82.269,
],
},
"rect2": Object {
"point": Array [
500,
346.81,
],
"size": Array [
200,
164.54,
],
},
}
`;
exports[`transform command snapshot tests transforms edges 7`] = `
Object {
"height": 500,
"maxX": 700,
"maxY": 600,
"minX": 111.38,
"minY": 100,
"width": 588.62,
}
`;
exports[`transform command snapshot tests transforms edges 8`] = `
Object {
"rect1": Object {
"point": Array [
111.38,
100,
],
"size": Array [
98.104,
100,
],
},
"rect2": Object {
"point": Array [
503.79,
400,
],
"size": Array [
196.21,
200,
],
},
}
`;
exports[`transform command transforms from the bottom edge 1`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
100,
120,
],
},
"rect2": Object {
"point": Array [
500,
460,
],
"size": Array [
200,
240,
],
},
}
`;
exports[`transform command transforms from the bottom-left corner 1`] = `
Object {
"rect1": Object {
"point": Array [
200,
100,
],
"size": Array [
83.333,
120,
],
},
"rect2": Object {
"point": Array [
533.33,
460,
],
"size": Array [
166.67,
240,
],
},
}
`;
exports[`transform command transforms from the bottom-right corner 1`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
116.67,
120,
],
},
"rect2": Object {
"point": Array [
566.67,
460,
],
"size": Array [
233.33,
240,
],
},
}
`;
exports[`transform command transforms from the left edge 1`] = `
Object {
"rect1": Object {
"point": Array [
200,
100,
],
"size": Array [
83.333,
100,
],
},
"rect2": Object {
"point": Array [
533.33,
400,
],
"size": Array [
166.67,
200,
],
},
}
`;
exports[`transform command transforms from the right edge 1`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
116.67,
100,
],
},
"rect2": Object {
"point": Array [
566.67,
400,
],
"size": Array [
233.33,
200,
],
},
}
`;
exports[`transform command transforms from the top edge 1`] = `
Object {
"rect1": Object {
"point": Array [
100,
200,
],
"size": Array [
100,
80,
],
},
"rect2": Object {
"point": Array [
500,
440,
],
"size": Array [
200,
160,
],
},
}
`;
exports[`transform command transforms from the top-left corner 1`] = `
Object {
"rect1": Object {
"point": Array [
200,
200,
],
"size": Array [
83.333,
80,
],
},
"rect2": Object {
"point": Array [
533.33,
440,
],
"size": Array [
166.67,
160,
],
},
}
`;
exports[`transform command transforms from the top-right corner 1`] = `
Object {
"rect1": Object {
"point": Array [
100,
200,
],
"size": Array [
116.67,
80,
],
},
"rect2": Object {
"point": Array [
566.67,
440,
],
"size": Array [
233.33,
160,
],
},
}
`;
exports[`transform command when transforming from the bottom-right corner does command 1`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
116.67,
120,
],
},
"rect2": Object {
"point": Array [
566.67,
460,
],
"size": Array [
233.33,
240,
],
},
}
`;
exports[`transform command when transforming from the bottom-right corner re-does command 1`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
116.67,
120,
],
},
"rect2": Object {
"point": Array [
566.67,
460,
],
"size": Array [
233.33,
240,
],
},
}
`;
exports[`transform command when transforming from the bottom-right corner un-does command 1`] = `
Object {
"rect1": Object {
"point": Array [
100,
100,
],
"size": Array [
100,
100,
],
},
"rect2": Object {
"point": Array [
500,
400,
],
"size": Array [
200,
200,
],
},
}
`;

View file

@ -0,0 +1,40 @@
import TestState from '../test-utils'
describe('align command', () => {
const tt = new TestState()
tt.resetDocumentState()
describe('when one item is selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
describe('when multiple items are selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
})

View file

@ -0,0 +1,21 @@
import TestState from '../test-utils'
describe('change page command', () => {
const tt = new TestState()
tt.resetDocumentState()
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})

View file

@ -0,0 +1,57 @@
import TestState from '../test-utils'
describe('delete page command', () => {
const tt = new TestState()
tt.resetDocumentState()
describe('when last page is selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
describe('when first page is selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
describe('when project only has one page', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
})

View file

@ -0,0 +1,40 @@
import TestState from '../test-utils'
describe('delete-selected command', () => {
const tt = new TestState()
tt.resetDocumentState()
describe('when one item is selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
describe('when multiple items are selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
})

View file

@ -1,7 +1,7 @@
import { ShapeType } from 'types' import { ShapeType } from 'types'
import TestState, { rectangleId, arrowId } from './test-utils' import TestState, { rectangleId, arrowId } from '../test-utils'
describe('deleting single shapes', () => { describe('delete command', () => {
const tt = new TestState() const tt = new TestState()
describe('deleting single shapes', () => { describe('deleting single shapes', () => {

View file

@ -0,0 +1,37 @@
import TestState from '../test-utils'
describe('distribute command', () => {
const tt = new TestState()
tt.resetDocumentState()
describe('when one item is selected', () => {
it('does not change anything', () => {
// TODO
null
})
})
describe('when two items are selected', () => {
it('does not change anything', () => {
// TODO
null
})
})
describe('when three or more items are selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
})

View file

@ -0,0 +1,21 @@
import TestState from '../test-utils'
describe('draw command', () => {
const tt = new TestState()
tt.resetDocumentState()
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})

View file

@ -0,0 +1,40 @@
import TestState from '../test-utils'
describe('duplicate command', () => {
const tt = new TestState()
tt.resetDocumentState()
describe('when one item is selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
describe('when multiple items are selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
})

View file

@ -0,0 +1,21 @@
import TestState from '../test-utils'
describe('edit command', () => {
const tt = new TestState()
tt.resetDocumentState()
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})

View file

@ -0,0 +1,21 @@
import TestState from '../test-utils'
describe('generate command', () => {
const tt = new TestState()
tt.resetDocumentState()
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})

View file

@ -0,0 +1,40 @@
import TestState from '../test-utils'
describe('group command', () => {
const tt = new TestState()
tt.resetDocumentState()
describe('when one item is selected', () => {
it('does not change anything', () => {
// TODO
null
})
})
describe('when multiple items are selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
it('groups shapes with different parents', () => {
// TODO
null
})
it('does not group a parent group shape and its child', () => {
// TODO
null
})
})

View file

@ -0,0 +1,40 @@
import TestState from '../test-utils'
describe('move-to-page command', () => {
const tt = new TestState()
tt.resetDocumentState()
describe('when one item is selected', () => {
it('does not change anything', () => {
// TODO
null
})
})
describe('when multiple items are selected', () => {
it('does command', () => {
// TODO
null
})
it('un-does command', () => {
// TODO
null
})
it('re-does command', () => {
// TODO
null
})
})
it('reparents children of groups to page', () => {
// TODO
null
})
it('correctly preserves moved groups', () => {
// TODO
null
})
})

View file

@ -0,0 +1,347 @@
import { Corner, Edge, RectangleShape, ShapeType } from 'types'
import { rng } from 'utils'
import TestState from '../test-utils'
describe('transform command', () => {
const tt = new TestState()
tt.resetDocumentState()
.createShape(
{
type: ShapeType.Rectangle,
point: [100, 100],
size: [100, 100],
childIndex: 1,
},
'rect1'
)
.createShape(
{
type: ShapeType.Rectangle,
point: [500, 400],
size: [200, 200],
childIndex: 2,
},
'rect2'
)
.clickShape('rect1')
.clickShape('rect2', { shiftKey: true })
.save()
function getSnapInfo() {
return {
rect1: {
point: tt.getShape<RectangleShape>('rect1').point,
size: tt.getShape<RectangleShape>('rect1').size,
},
rect2: {
point: tt.getShape<RectangleShape>('rect2').point,
size: tt.getShape<RectangleShape>('rect2').size,
},
}
}
it('sets up initial bounds', () => {
expect(tt.selectedIds).toEqual(['rect1', 'rect2'])
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 100,
maxX: 700,
maxY: 600,
width: 600,
height: 500,
})
})
describe('when transforming from the bottom-right corner', () => {
it('does command', () => {
// Restore the saved data state, with the initial selection
tt.restore()
// Move the bounds handle
tt.startClickingBoundsHandle(Corner.BottomRight)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 100,
maxX: 800,
maxY: 700,
width: 700,
height: 600,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('un-does command', () => {
// Repeat the same actions, but add an undo at the end
tt.restore()
.startClickingBoundsHandle(Corner.BottomRight)
.movePointerBy([100, 100])
.stopClickingBounds()
.undo()
// Expect the bounds to be the initial bounds
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 100,
maxX: 700,
maxY: 600,
width: 600,
height: 500,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('re-does command', () => {
// Repeat the same actions but add an undo and a redo at the end
tt.restore()
.startClickingBoundsHandle(Corner.BottomRight)
.movePointerBy([100, 100])
.stopClickingBounds()
.undo()
.redo()
// Expect the bounds to be the transformed bounds
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 100,
maxX: 800,
maxY: 700,
width: 700,
height: 600,
})
expect(getSnapInfo()).toMatchSnapshot()
})
})
// From here on, let's assume that the undo and redos work as expected,
// so let's only test the command's execution.
it('transforms from the top edge', () => {
tt.restore()
.startClickingBoundsHandle(Edge.Top)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 200,
maxX: 700,
maxY: 600,
width: 600,
height: 400,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('transforms from the right edge', () => {
tt.restore()
.startClickingBoundsHandle(Edge.Right)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 100,
maxX: 800,
maxY: 600,
width: 700,
height: 500,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('transforms from the bottom edge', () => {
tt.restore()
.startClickingBoundsHandle(Edge.Bottom)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 100,
maxX: 700,
maxY: 700,
width: 600,
height: 600,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('transforms from the left edge', () => {
tt.restore()
.startClickingBoundsHandle(Edge.Left)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 200,
minY: 100,
maxX: 700,
maxY: 600,
width: 500,
height: 500,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('transforms from the top-left corner', () => {
tt.restore()
.startClickingBoundsHandle(Corner.TopLeft)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 200,
minY: 200,
maxX: 700,
maxY: 600,
width: 500,
height: 400,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('transforms from the top-right corner', () => {
tt.restore()
.startClickingBoundsHandle(Corner.TopRight)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 200,
maxX: 800,
maxY: 600,
width: 700,
height: 400,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('transforms from the bottom-right corner', () => {
tt.restore()
.startClickingBoundsHandle(Corner.BottomRight)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 100,
minY: 100,
maxX: 800,
maxY: 700,
width: 700,
height: 600,
})
expect(getSnapInfo()).toMatchSnapshot()
})
it('transforms from the bottom-left corner', () => {
tt.restore()
.startClickingBoundsHandle(Corner.BottomLeft)
.movePointerBy([100, 100])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchObject({
minX: 200,
minY: 100,
maxX: 700,
maxY: 700,
width: 500,
height: 600,
})
expect(getSnapInfo()).toMatchSnapshot()
})
describe('snapshot tests', () => {
it('transforms corners', () => {
const getRandom = rng('transform-tests-random-number-generator')
for (const corner of Object.values(Corner)) {
tt.restore()
.startClickingBoundsHandle(corner)
.movePointerBy([getRandom() * 200, getRandom() * 200])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchSnapshot()
expect(getSnapInfo()).toMatchSnapshot()
}
})
it('transforms edges', () => {
const getRandom = rng('transform-tests-random-number-generator')
for (const edge of Object.values(Edge)) {
tt.restore()
.startClickingBoundsHandle(edge)
.movePointerBy([getRandom() * 200, getRandom() * 200])
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchSnapshot()
expect(getSnapInfo()).toMatchSnapshot()
}
})
it('shift-transforms corners', () => {
const getRandom = rng('transform-tests-random-number-generator')
for (const corner of Object.values(Corner)) {
tt.restore()
.startClickingBoundsHandle(corner)
.movePointerBy([getRandom() * 200, getRandom() * 200], {
shiftKey: true,
})
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchSnapshot()
expect(getSnapInfo()).toMatchSnapshot()
}
})
it('shift-transforms edges', () => {
const getRandom = rng('transform-tests-random-number-generator')
for (const edge of Object.values(Edge)) {
tt.restore()
.startClickingBoundsHandle(edge)
.movePointerBy([getRandom() * 200, getRandom() * 200], {
shiftKey: true,
})
.stopClickingBounds()
// Ensure the bounds have been transformed
expect(tt.state.values.selectedBounds).toMatchSnapshot()
expect(getSnapInfo()).toMatchSnapshot()
}
})
})
})

View file

@ -1,11 +1,9 @@
import state from 'state' import TestState from '../test-utils'
import * as json from './__mocks__/document.json'
state.reset() describe('translate command', () => {
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) }) const tt = new TestState()
state.send('CLEARED_PAGE') tt.resetDocumentState()
describe('translates shapes', () => {
it('translates a single selected shape', () => { it('translates a single selected shape', () => {
// TODO // TODO
null null

View file

@ -1,6 +1,6 @@
import TestState from './test-utils' import TestState from './test-utils'
describe('locked shapes', () => { describe('lock command', () => {
const tt = new TestState() const tt = new TestState()
tt.resetDocumentState() tt.resetDocumentState()

View file

@ -2,8 +2,8 @@ import _state from 'state'
import tld from 'utils/tld' import tld from 'utils/tld'
import inputs from 'state/inputs' import inputs from 'state/inputs'
import { createShape, getShapeUtils } from 'state/shape-utils' import { createShape, getShapeUtils } from 'state/shape-utils'
import { Data, Shape, ShapeType, ShapeUtility } from 'types' import { Corner, Data, Edge, Shape, ShapeType, ShapeUtility } from 'types'
import { deepCompareArrays, setToArray, uniqueId, vec } from 'utils' import { deepClone, deepCompareArrays, uniqueId, vec } from 'utils'
import * as mockDocument from './__mocks__/document.json' import * as mockDocument from './__mocks__/document.json'
type State = typeof _state type State = typeof _state
@ -22,9 +22,12 @@ interface PointerOptions {
class TestState { class TestState {
state: State state: State
snapshot: Data
constructor() { constructor() {
this.state = _state this.state = _state
this.state.send('TOGGLED_TEST_MODE')
this.snapshot = deepClone(this.state.data)
this.reset() this.reset()
} }
@ -57,7 +60,7 @@ class TestState {
*``` *```
*/ */
resetDocumentState(): TestState { resetDocumentState(): TestState {
this.state.send('RESET_DOCUMENT_STATE') this.state.send('RESET_DOCUMENT_STATE').send('TOGGLED_TEST_MODE')
return this return this
} }
@ -122,13 +125,13 @@ class TestState {
idsAreSelected(ids: string[], strict = true): boolean { idsAreSelected(ids: string[], strict = true): boolean {
const selectedIds = tld.getSelectedIds(this.data) const selectedIds = tld.getSelectedIds(this.data)
return ( return (
(strict ? selectedIds.size === ids.length : true) && (strict ? selectedIds.length === ids.length : true) &&
ids.every((id) => selectedIds.has(id)) ids.every((id) => selectedIds.includes(id))
) )
} }
get selectedIds(): string[] { get selectedIds(): string[] {
return setToArray(tld.getSelectedIds(this.data)) return tld.getSelectedIds(this.data)
} }
/** /**
@ -340,6 +343,63 @@ class TestState {
return this return this
} }
/**
* Start clicking bounds.
*
* ### Example
*
*```ts
* tt.startClickingBounds()
*```
*/
startClickingBounds(options: PointerOptions = {}): TestState {
this.state.send(
'POINTED_BOUNDS',
inputs.pointerDown(TestState.point(options), 'bounds')
)
return this
}
/**
* Stop clicking the bounding box.
*
* ### Example
*
*```ts
* tt.stopClickingBounds()
*```
*/
stopClickingBounds(options: PointerOptions = {}): TestState {
this.state.send(
'STOPPED_POINTING',
inputs.pointerUp(TestState.point(options), 'bounds')
)
return this
}
/**
* Start clicking a bounds handle.
*
* ### Example
*
*```ts
* tt.startClickingBoundsHandle(Edge.Top)
*```
*/
startClickingBoundsHandle(
handle: Corner | Edge | 'center',
options: PointerOptions = {}
): TestState {
this.state.send(
'POINTED_BOUNDS_HANDLE',
inputs.pointerDown(TestState.point(options), handle)
)
return this
}
/** /**
* Move the pointer to a new point, or to several points in order. * Move the pointer to a new point, or to several points in order.
* *
@ -572,6 +632,35 @@ class TestState {
return this return this
} }
/**
* Save a snapshot of the state's current data.
*
* ### Example
*
*```ts
* tt.save()
*```
*/
save(): TestState {
this.snapshot = deepClone(this.data)
return this
}
/**
* Restore the state's saved data.
*
* ### Example
*
*```ts
* tt.save()
* tt.restore()
*```
*/
restore(): TestState {
this.state.forceData(this.snapshot)
return this
}
/** /**
* Get the state's current data. * Get the state's current data.
* *

View file

@ -1,91 +0,0 @@
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
})
})

View file

@ -1840,14 +1840,6 @@ type RequiredKeys<T> = {
return Array.from(new Set(items).values()) return Array.from(new Set(items).values())
} }
/**
* Convert a set to an array.
* @param set
*/
static setToArray<T>(set: Set<T>): T[] {
return Array.from(set.values())
}
/** /**
* Get the outer of between a circle and a point. * Get the outer of between a circle and a point.
* @param C The circle's center. * @param C The circle's center.

View file

@ -86,11 +86,11 @@ function ShapesFunctions() {
}) })
const hasSelection = useSelector((s) => { const hasSelection = useSelector((s) => {
return tld.getSelectedIds(s.data).size > 0 return tld.getSelectedIds(s.data).length > 0
}) })
const hasMultipleSelection = useSelector((s) => { const hasMultipleSelection = useSelector((s) => {
return tld.getSelectedIds(s.data).size > 1 return tld.getSelectedIds(s.data).length > 1
}) })
return ( return (

View file

@ -7,13 +7,13 @@ import {
SizeStyle, SizeStyle,
} from 'types' } from 'types'
import { createShape, getShapeUtils } from 'state/shape-utils' import { createShape, getShapeUtils } from 'state/shape-utils'
import { setToArray, uniqueId } from 'utils' import { uniqueId } from 'utils'
import Vec from 'utils/vec' import Vec from 'utils/vec'
export const codeShapes = new Set<CodeShape<Shape>>([]) export const codeShapes = new Set<CodeShape<Shape>>([])
function getOrderedShapes() { function getOrderedShapes() {
return setToArray(codeShapes).sort( return Array.from(codeShapes.values()).sort(
(a, b) => a.shape.childIndex - b.shape.childIndex (a, b) => a.shape.childIndex - b.shape.childIndex
) )
} }

View file

@ -1,5 +1,4 @@
import { Data } from 'types' import { Data } from 'types'
import { setToArray } from 'utils'
import tld from 'utils/tld' import tld from 'utils/tld'
/* ------------------ Command Class ----------------- */ /* ------------------ Command Class ----------------- */
@ -85,7 +84,7 @@ export class BaseCommand<T extends any> {
export default class Command extends BaseCommand<Data> { export default class Command extends BaseCommand<Data> {
saveSelectionState = (data: Data): ((next: Data) => void) => { saveSelectionState = (data: Data): ((next: Data) => void) => {
const { currentPageId } = data const { currentPageId } = data
const selectedIds = setToArray(tld.getSelectedIds(data)) const selectedIds = [...tld.getSelectedIds(data)]
return (next: Data) => { return (next: Data) => {
next.currentPageId = currentPageId next.currentPageId = currentPageId
next.hoveredId = undefined next.hoveredId = undefined

View file

@ -54,7 +54,7 @@ function getSnapshot(data: Data) {
const pageState: PageState = { const pageState: PageState = {
id, id,
selectedIds: new Set([]), selectedIds: [],
camera: { camera: {
point: [0, 0], point: [0, 0],
zoom: 1, zoom: 1,

View file

@ -100,7 +100,7 @@ export default function groupCommand(data: Data): void {
getShapeUtils(oldParent).setProperty( getShapeUtils(oldParent).setProperty(
oldParent, oldParent,
'children', 'children',
oldParent.children.filter((id) => !oldSelectedIds.has(id)) oldParent.children.filter((id) => !oldSelectedIds.includes(id))
) )
} }

View file

@ -1,7 +1,7 @@
import Command from './command' import Command from './command'
import history from '../history' import history from '../history'
import { Data } from 'types' import { Data } from 'types'
import { setToArray, uniqueArray } from 'utils' import { uniqueArray } from 'utils'
import tld from 'utils/tld' import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
import storage from 'state/storage' import storage from 'state/storage'
@ -9,7 +9,7 @@ import storage from 'state/storage'
export default function moveToPageCommand(data: Data, newPageId: string): void { export default function moveToPageCommand(data: Data, newPageId: string): void {
const { currentPageId: oldPageId } = data const { currentPageId: oldPageId } = data
const oldPage = tld.getPage(data) const oldPage = tld.getPage(data)
const selectedIds = setToArray(tld.getSelectedIds(data)) const selectedIds = [...tld.getSelectedIds(data)]
const idsToMove = uniqueArray( const idsToMove = uniqueArray(
...selectedIds.flatMap((id) => tld.getDocumentBranch(data, id)) ...selectedIds.flatMap((id) => tld.getDocumentBranch(data, id))
@ -59,7 +59,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
}) })
// Clear the current page state's selected ids // Clear the current page state's selected ids
tld.getPageState(data).selectedIds.clear() tld.setSelectedIds(data, [])
// Save the "from" page // Save the "from" page
storage.savePage(data, data.document.id, fromPageId) storage.savePage(data, data.document.id, fromPageId)
@ -83,7 +83,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
}) })
// Select the selected ids on the new page // Select the selected ids on the new page
tld.getPageState(data).selectedIds = new Set(selectedIds) tld.setSelectedIds(data, [...selectedIds])
// Move to the new page // Move to the new page
data.currentPageId = toPageId data.currentPageId = toPageId
@ -113,7 +113,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
delete fromPage.shapes[shape.id] delete fromPage.shapes[shape.id]
}) })
tld.getPageState(data).selectedIds.clear() tld.setSelectedIds(data, [])
storage.savePage(data, data.document.id, fromPageId) storage.savePage(data, data.document.id, fromPageId)
@ -138,7 +138,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
} }
}) })
tld.getPageState(data).selectedIds = new Set(selectedIds) tld.setSelectedIds(data, [...selectedIds])
data.currentPageId = toPageId data.currentPageId = toPageId
}, },

View file

@ -1,14 +1,13 @@
import Command from './command' import Command from './command'
import history from '../history' import history from '../history'
import { Data, MoveType, Shape } from 'types' import { Data, MoveType, Shape } from 'types'
import { setToArray } from 'utils'
import tld from 'utils/tld' import tld from 'utils/tld'
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
export default function moveCommand(data: Data, type: MoveType): void { export default function moveCommand(data: Data, type: MoveType): void {
const page = tld.getPage(data) const page = tld.getPage(data)
const selectedIds = setToArray(tld.getSelectedIds(data)) const selectedIds = [...tld.getSelectedIds(data)]
const initialIndices = Object.fromEntries( const initialIndices = Object.fromEntries(
selectedIds.map((id) => [id, page.shapes[id].childIndex]) selectedIds.map((id) => [id, page.shapes[id].childIndex])

View file

@ -1,7 +1,7 @@
import Command from './command' import Command from './command'
import history from '../history' import history from '../history'
import { Data, Shape } from 'types' import { Data, Shape } from 'types'
import { getCommonBounds, setToArray } from 'utils' import { getCommonBounds } from 'utils'
import tld from 'utils/tld' import tld from 'utils/tld'
import { uniqueId } from 'utils/utils' import { uniqueId } from 'utils/utils'
import vec from 'utils/vec' import vec from 'utils/vec'
@ -26,7 +26,7 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
initialShapes.map((shape) => [shape.id, uniqueId()]) initialShapes.map((shape) => [shape.id, uniqueId()])
) )
const oldSelectedIds = setToArray(tld.getSelectedIds(data)) const oldSelectedIds = [...tld.getSelectedIds(data)]
history.execute( history.execute(
data, data,

View file

@ -2,7 +2,7 @@ import Command from './command'
import history from '../history' import history from '../history'
import { Data, ShapeStyles } from 'types' import { Data, ShapeStyles } from 'types'
import tld from 'utils/tld' import tld from 'utils/tld'
import { deepClone, setToArray } from 'utils' import { deepClone } from 'utils'
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
export default function styleCommand( export default function styleCommand(
@ -11,7 +11,7 @@ export default function styleCommand(
): void { ): void {
const page = tld.getPage(data) const page = tld.getPage(data)
const selectedIds = setToArray(tld.getSelectedIds(data)) const selectedIds = [...tld.getSelectedIds(data)]
const shapesToStyle = selectedIds const shapesToStyle = selectedIds
.flatMap((id) => tld.getDocumentBranch(data, id)) .flatMap((id) => tld.getDocumentBranch(data, id))

View file

@ -1,5 +1,5 @@
import { DrawShape, PointerInfo } from 'types' import { DrawShape, PointerInfo } from 'types'
import { deepClone, setToArray } from 'utils' import { deepClone } from 'utils'
import tld from 'utils/tld' import tld from 'utils/tld'
import { freeze } from 'immer' import { freeze } from 'immer'
import session from './session' import session from './session'
@ -56,7 +56,7 @@ export function fastDrawUpdate(info: PointerInfo): void {
info.shiftKey info.shiftKey
) )
const selectedId = setToArray(tld.getSelectedIds(data))[0] const selectedId = [...tld.getSelectedIds(data)][0]
const { shapes } = data.document.pages[data.currentPageId] const { shapes } = data.document.pages[data.currentPageId]

View file

@ -2,7 +2,6 @@ import { Data } from 'types'
import clipboard from './clipboard' import clipboard from './clipboard'
import state from './state' import state from './state'
import { isDraft, current } from 'immer' import { isDraft, current } from 'immer'
import { setToArray } from 'utils'
import tld from 'utils/tld' import tld from 'utils/tld'
import inputs from './inputs' import inputs from './inputs'
@ -59,9 +58,9 @@ class Logger {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.snapshotStart.pageStates[data.currentPageId].selectedIds = setToArray( this.snapshotStart.pageStates[data.currentPageId].selectedIds = [
tld.getSelectedIds(data) ...tld.getSelectedIds(data),
) ]
return this return this
} }
@ -84,9 +83,9 @@ class Logger {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.snapshotEnd.pageStates[data.currentPageId].selectedIds = setToArray( this.snapshotEnd.pageStates[data.currentPageId].selectedIds = [
tld.getSelectedIds(data) ...tld.getSelectedIds(data),
) ]
// if (window.confirm('Stopped logging. Copy to clipboard?')) { // if (window.confirm('Stopped logging. Copy to clipboard?')) {
// this.copyToJson() // this.copyToJson()
@ -138,9 +137,9 @@ class Logger {
this.isSimulating = true this.isSimulating = true
try { try {
data.pageStates[data.currentPageId].selectedIds = new Set( data.pageStates[data.currentPageId].selectedIds = [
start.pageStates[start.currentPageId].selectedIds ...start.pageStates[start.currentPageId].selectedIds,
) ]
state.send('RESET_DOCUMENT_STATE').forceData(start) state.send('RESET_DOCUMENT_STATE').forceData(start)

View file

@ -1,7 +1,7 @@
import { Bounds, Data, ShapeType } from 'types' import { Bounds, Data, ShapeType } from 'types'
import BaseSession from './base-session' import BaseSession from './base-session'
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
import { deepClone, getBoundsFromPoints, setToArray } from 'utils' import { deepClone, getBoundsFromPoints } from 'utils'
import vec from 'utils/vec' import vec from 'utils/vec'
import tld from 'utils/tld' import tld from 'utils/tld'
@ -24,10 +24,10 @@ export default class BrushSession extends BaseSession {
const hits = new Set<string>([]) const hits = new Set<string>([])
const selectedIds = new Set(snapshot.selectedIds) const selectedIds = [...snapshot.selectedIds]
for (const id in snapshot.shapeHitTests) { for (const id in snapshot.shapeHitTests) {
if (selectedIds.has(id)) continue if (selectedIds.includes(id)) continue
const { test, selectId } = snapshot.shapeHitTests[id] const { test, selectId } = snapshot.shapeHitTests[id]
if (!hits.has(selectId)) { if (!hits.has(selectId)) {
@ -35,11 +35,11 @@ export default class BrushSession extends BaseSession {
hits.add(selectId) hits.add(selectId)
// When brushing a shape, select its top group parent. // When brushing a shape, select its top group parent.
if (!selectedIds.has(selectId)) { if (!selectedIds.includes(selectId)) {
selectedIds.add(selectId) selectedIds.push(selectId)
} }
} else if (selectedIds.has(selectId)) { } else if (selectedIds.includes(selectId)) {
selectedIds.delete(selectId) selectedIds.splice(selectedIds.indexOf(selectId), 1)
} }
} }
} }
@ -73,12 +73,15 @@ export function getBrushSnapshot(data: Data) {
.getShapes(cData) .getShapes(cData)
.filter((shape) => shape.type !== ShapeType.Group && !shape.isHidden) .filter((shape) => shape.type !== ShapeType.Group && !shape.isHidden)
.filter( .filter(
(shape) => !(selectedIds.has(shape.id) || selectedIds.has(shape.parentId)) (shape) =>
!(
selectedIds.includes(shape.id) || selectedIds.includes(shape.parentId)
)
) )
.map(deepClone) .map(deepClone)
return { return {
selectedIds: setToArray(selectedIds), selectedIds: [...selectedIds],
shapeHitTests: Object.fromEntries( shapeHitTests: Object.fromEntries(
shapesToTest.map((shape) => { shapesToTest.map((shape) => {
return [ return [

View file

@ -133,22 +133,21 @@ const rectangle = registerShapeUtils<RectangleShape>({
transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) { transform(shape, bounds, { initialShape, transformOrigin, scaleX, scaleY }) {
if (shape.rotation === 0 && !shape.isAspectRatioLocked) { if (shape.rotation === 0 && !shape.isAspectRatioLocked) {
shape.size = [bounds.width, bounds.height] shape.size = vec.round([bounds.width, bounds.height])
shape.point = [bounds.minX, bounds.minY] shape.point = vec.round([bounds.minX, bounds.minY])
} else { } else {
shape.size = vec.mul( shape.size = vec.round(
initialShape.size, vec.mul(initialShape.size, Math.min(Math.abs(scaleX), Math.abs(scaleY)))
Math.min(Math.abs(scaleX), Math.abs(scaleY))
) )
shape.point = [ shape.point = vec.round([
bounds.minX + bounds.minX +
(bounds.width - shape.size[0]) * (bounds.width - shape.size[0]) *
(scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]), (scaleX < 0 ? 1 - transformOrigin[0] : transformOrigin[0]),
bounds.minY + bounds.minY +
(bounds.height - shape.size[1]) * (bounds.height - shape.size[1]) *
(scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]), (scaleY < 0 ? 1 - transformOrigin[1] : transformOrigin[1]),
] ])
shape.rotation = shape.rotation =
(scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0) (scaleX < 0 && scaleY >= 0) || (scaleY < 0 && scaleX >= 0)
@ -160,8 +159,8 @@ const rectangle = registerShapeUtils<RectangleShape>({
}, },
transformSingle(shape, bounds) { transformSingle(shape, bounds) {
shape.size = [bounds.width, bounds.height] shape.size = vec.round([bounds.width, bounds.height])
shape.point = [bounds.minX, bounds.minY] shape.point = vec.round([bounds.minX, bounds.minY])
return this return this
}, },
}) })

View file

@ -13,7 +13,6 @@ import {
getCommonBounds, getCommonBounds,
rotateBounds, rotateBounds,
getBoundsCenter, getBoundsCenter,
setToArray,
deepClone, deepClone,
pointInBounds, pointInBounds,
uniqueId, uniqueId,
@ -43,6 +42,7 @@ const initialData: Data = {
isReadOnly: false, isReadOnly: false,
settings: { settings: {
fontSize: 13, fontSize: 13,
isTestMode: false,
isDarkMode: false, isDarkMode: false,
isCodeOpen: false, isCodeOpen: false,
isDebugMode: false, isDebugMode: false,
@ -133,7 +133,7 @@ for (let i = 0; i < count; i++) {
pageStates: { pageStates: {
page1: { page1: {
id: 'page1', id: 'page1',
selectedIds: new Set([]), selectedIds: [],
camera: { camera: {
point: [0, 0], point: [0, 0],
zoom: 1, zoom: 1,
@ -147,6 +147,7 @@ const state = createState({
on: { on: {
TOGGLED_DEBUG_PANEL: 'toggleDebugPanel', TOGGLED_DEBUG_PANEL: 'toggleDebugPanel',
TOGGLED_DEBUG_MODE: 'toggleDebugMode', TOGGLED_DEBUG_MODE: 'toggleDebugMode',
TOGGLED_TEST_MODE: 'toggleTestMode',
TOGGLED_LOGGER: 'toggleLogger', TOGGLED_LOGGER: 'toggleLogger',
COPIED_DEBUG_LOG: 'copyDebugLog', COPIED_DEBUG_LOG: 'copyDebugLog',
LOADED_FROM_SNAPSHOT: { LOADED_FROM_SNAPSHOT: {
@ -588,7 +589,10 @@ const state = createState({
onEnter: 'startTransformSession', onEnter: 'startTransformSession',
onExit: 'completeSession', onExit: 'completeSession',
on: { on: {
// MOVED_POINTER: 'updateTransformSession', using hacks.fastTransform MOVED_POINTER: {
ifAny: ['isSimulating', 'isTestMode'],
do: 'updateTransformSession',
},
PANNED_CAMERA: 'updateTransformSession', PANNED_CAMERA: 'updateTransformSession',
PRESSED_SHIFT_KEY: 'keyUpdateTransformSession', PRESSED_SHIFT_KEY: 'keyUpdateTransformSession',
RELEASED_SHIFT_KEY: 'keyUpdateTransformSession', RELEASED_SHIFT_KEY: 'keyUpdateTransformSession',
@ -638,7 +642,7 @@ const state = createState({
'startBrushSession', 'startBrushSession',
], ],
on: { on: {
// MOVED_POINTER: 'updateBrushSession', using hacks.fastBrushSelect MOVED_POINTER: { if: 'isTestMode', do: 'updateBrushSession' },
PANNED_CAMERA: 'updateBrushSession', PANNED_CAMERA: 'updateBrushSession',
STOPPED_POINTING: { to: 'selecting' }, STOPPED_POINTING: { to: 'selecting' },
STARTED_PINCHING: { to: 'pinching' }, STARTED_PINCHING: { to: 'pinching' },
@ -694,7 +698,7 @@ const state = createState({
}, },
pinching: { pinching: {
on: { on: {
// PINCHED: { do: 'pinchCamera' }, using hacks.fastPinchCamera PINCHED: { if: 'isTestMode', do: 'pinchCamera' },
}, },
initial: 'selectPinching', initial: 'selectPinching',
onExit: { secretlyDo: 'updateZoomCSS' }, onExit: { secretlyDo: 'updateZoomCSS' },
@ -760,7 +764,7 @@ const state = createState({
RELEASED_SHIFT: 'keyUpdateDrawSession', RELEASED_SHIFT: 'keyUpdateDrawSession',
PANNED_CAMERA: 'updateDrawSession', PANNED_CAMERA: 'updateDrawSession',
MOVED_POINTER: { MOVED_POINTER: {
if: 'isSimulating', ifAny: ['isSimulating', 'isTestMode'],
do: 'updateDrawSession', do: 'updateDrawSession',
}, },
}, },
@ -1115,6 +1119,9 @@ const state = createState({
isSimulating() { isSimulating() {
return logger.isSimulating return logger.isSimulating
}, },
isTestMode(data) {
return data.settings.isTestMode
},
isEditingShape(data, payload: { id: string }) { isEditingShape(data, payload: { id: string }) {
return payload.id === data.editingId return payload.id === data.editingId
}, },
@ -1131,7 +1138,7 @@ const state = createState({
return tld.getShape(data, payload.target)?.type === ShapeType.Text return tld.getShape(data, payload.target)?.type === ShapeType.Text
}, },
isPointingBounds(data, payload: PointerInfo) { isPointingBounds(data, payload: PointerInfo) {
return tld.getSelectedIds(data).size > 0 && payload.target === 'bounds' return tld.getSelectedIds(data).length > 0 && payload.target === 'bounds'
}, },
isPointingShape(data, payload: PointerInfo) { isPointingShape(data, payload: PointerInfo) {
return ( return (
@ -1156,7 +1163,7 @@ const state = createState({
return payload.target !== undefined return payload.target !== undefined
}, },
isPointedShapeSelected(data) { isPointedShapeSelected(data) {
return tld.getSelectedIds(data).has(data.pointedId) return tld.getSelectedIds(data).includes(data.pointedId)
}, },
isPressingShiftKey(data, payload: PointerInfo) { isPressingShiftKey(data, payload: PointerInfo) {
return payload.shiftKey return payload.shiftKey
@ -1192,13 +1199,13 @@ const state = createState({
return payload.target === 'rotate' return payload.target === 'rotate'
}, },
hasSelection(data) { hasSelection(data) {
return tld.getSelectedIds(data).size > 0 return tld.getSelectedIds(data).length > 0
}, },
hasSingleSelection(data) { hasSingleSelection(data) {
return tld.getSelectedIds(data).size === 1 return tld.getSelectedIds(data).length === 1
}, },
hasMultipleSelection(data) { hasMultipleSelection(data) {
return tld.getSelectedIds(data).size > 1 return tld.getSelectedIds(data).length > 1
}, },
hasCurrentParentShape(data) { hasCurrentParentShape(data) {
return data.currentParentId !== data.currentPageId return data.currentParentId !== data.currentPageId
@ -1230,6 +1237,9 @@ const state = createState({
toggleDebugMode(data) { toggleDebugMode(data) {
data.settings.isDebugMode = !data.settings.isDebugMode data.settings.isDebugMode = !data.settings.isDebugMode
}, },
toggleTestMode(data) {
data.settings.isTestMode = !data.settings.isTestMode
},
toggleDebugPanel(data) { toggleDebugPanel(data) {
data.settings.isDebugOpen = !data.settings.isDebugOpen data.settings.isDebugOpen = !data.settings.isDebugOpen
}, },
@ -1304,7 +1314,7 @@ const state = createState({
data.pageStates = { data.pageStates = {
[newPageId]: { [newPageId]: {
id: newPageId, id: newPageId,
selectedIds: new Set(), selectedIds: [],
camera: { camera: {
point: [0, 0], point: [0, 0],
zoom: 1, zoom: 1,
@ -1464,7 +1474,7 @@ const state = createState({
// Handles // Handles
doublePointHandle(data, payload: PointerInfo) { doublePointHandle(data, payload: PointerInfo) {
const id = setToArray(tld.getSelectedIds(data))[0] const id = tld.getSelectedIds(data)[0]
commands.doublePointHandle(data, id, payload) commands.doublePointHandle(data, id, payload)
}, },
@ -1511,7 +1521,7 @@ const state = createState({
) { ) {
const point = tld.screenToWorld(inputs.pointer.origin, data) const point = tld.screenToWorld(inputs.pointer.origin, data)
session.begin( session.begin(
tld.getSelectedIds(data).size === 1 tld.getSelectedIds(data).length === 1
? new Sessions.TransformSingleSession(data, payload.target, point) ? new Sessions.TransformSingleSession(data, payload.target, point)
: new Sessions.TransformSession(data, payload.target, point) : new Sessions.TransformSession(data, payload.target, point)
) )
@ -1618,7 +1628,7 @@ const state = createState({
inputs.clear() inputs.clear()
}, },
deselectAll(data) { deselectAll(data) {
tld.getSelectedIds(data).clear() tld.setSelectedIds(data, [])
}, },
selectAll(data) { selectAll(data) {
tld.setSelectedIds( tld.setSelectedIds(
@ -1673,10 +1683,10 @@ const state = createState({
pullPointedIdFromSelectedIds(data) { pullPointedIdFromSelectedIds(data) {
const { pointedId } = data const { pointedId } = data
const selectedIds = tld.getSelectedIds(data) const selectedIds = tld.getSelectedIds(data)
selectedIds.delete(pointedId) selectedIds.splice(selectedIds.indexOf(pointedId), 1)
}, },
pushPointedIdToSelectedIds(data) { pushPointedIdToSelectedIds(data) {
tld.getSelectedIds(data).add(data.pointedId) tld.getSelectedIds(data).push(data.pointedId)
}, },
moveSelection(data, payload: { type: MoveType }) { moveSelection(data, payload: { type: MoveType }) {
commands.move(data, payload.type) commands.move(data, payload.type)
@ -1732,7 +1742,7 @@ const state = createState({
data.editingId = selectedShape.id data.editingId = selectedShape.id
} }
tld.getPageState(data).selectedIds = new Set([selectedShape.id]) tld.getPageState(data).selectedIds = [selectedShape.id]
}, },
clearEditingId(data) { clearEditingId(data) {
data.editingId = null data.editingId = null
@ -2094,7 +2104,7 @@ const state = createState({
}, },
values: { values: {
selectedIds(data) { selectedIds(data) {
return setToArray(tld.getSelectedIds(data)) return tld.getSelectedIds(data)
}, },
selectedBounds(data) { selectedBounds(data) {
return getSelectionBounds(data) return getSelectionBounds(data)
@ -2107,7 +2117,7 @@ const state = createState({
.sort((a, b) => a.childIndex - b.childIndex) .sort((a, b) => a.childIndex - b.childIndex)
}, },
selectedStyle(data) { selectedStyle(data) {
const selectedIds = setToArray(tld.getSelectedIds(data)) const selectedIds = tld.getSelectedIds(data)
const { currentStyle } = data const { currentStyle } = data
if (selectedIds.length === 0) { if (selectedIds.length === 0) {
@ -2195,9 +2205,9 @@ function getSelectionBounds(data: Data) {
const shapes = tld.getSelectedShapes(data) const shapes = tld.getSelectedShapes(data)
if (selectedIds.size === 0) return null if (selectedIds.length === 0) return null
if (selectedIds.size === 1) { if (selectedIds.length === 1) {
if (!shapes[0]) { if (!shapes[0]) {
console.warn('Could not find that shape! Clearing selected IDs.') console.warn('Could not find that shape! Clearing selected IDs.')
tld.setSelectedIds(data, []) tld.setSelectedIds(data, [])

View file

@ -1,5 +1,5 @@
import { Data, PageState, TLDocument } from 'types' import { Data, PageState, TLDocument } from 'types'
import { decompress, compress, setToArray } from 'utils' import { decompress, compress } from 'utils'
import state from './state' import state from './state'
import { uniqueId } from 'utils/utils' import { uniqueId } from 'utils/utils'
import * as idb from 'idb-keyval' import * as idb from 'idb-keyval'
@ -132,14 +132,11 @@ class Storage {
if (savedPageState !== null) { if (savedPageState !== null) {
// If we've found a page state in local storage, set it into state. // If we've found a page state in local storage, set it into state.
data.pageStates[pageId] = JSON.parse(decompress(savedPageState)) data.pageStates[pageId] = JSON.parse(decompress(savedPageState))
data.pageStates[pageId].selectedIds = new Set(
data.pageStates[pageId].selectedIds
)
} else { } else {
// Or else create a new one. // Or else create a new one.
data.pageStates[pageId] = { data.pageStates[pageId] = {
id: pageId, id: pageId,
selectedIds: new Set([]), selectedIds: [],
camera: { camera: {
point: [0, 0], point: [0, 0],
zoom: 1, zoom: 1,
@ -161,13 +158,13 @@ class Storage {
throw new Error('Page state id not in document') throw new Error('Page state id not in document')
} }
pageState.selectedIds = new Set([]) pageState.selectedIds = []
data.pageStates[pageState.id] = pageState data.pageStates[pageState.id] = pageState
data.currentPageId = pageState.id data.currentPageId = pageState.id
} catch (e) { } catch (e) {
data.pageStates[data.currentPageId] = { data.pageStates[data.currentPageId] = {
id: data.currentPageId, id: data.currentPageId,
selectedIds: new Set([]), selectedIds: [],
camera: { camera: {
point: [0, 0], point: [0, 0],
zoom: 1, zoom: 1,
@ -249,7 +246,7 @@ class Storage {
storageId(fileId, 'pageState', pageId), storageId(fileId, 'pageState', pageId),
JSON.stringify({ JSON.stringify({
...currentPageState, ...currentPageState,
selectedIds: setToArray(currentPageState.selectedIds), selectedIds: [...currentPageState.selectedIds],
}) })
) )
} }
@ -286,7 +283,6 @@ class Storage {
// If we have a page, move it into state // If we have a page, move it into state
const restored: PageState = JSON.parse(savedPageState) const restored: PageState = JSON.parse(savedPageState)
data.pageStates[pageId] = restored data.pageStates[pageId] = restored
data.pageStates[pageId].selectedIds = new Set(restored.selectedIds)
} else { } else {
data.pageStates[pageId] = { data.pageStates[pageId] = {
id: pageId, id: pageId,
@ -294,7 +290,7 @@ class Storage {
point: [0, 0], point: [0, 0],
zoom: 1, zoom: 1,
}, },
selectedIds: new Set([]), selectedIds: [],
} }
} }

View file

@ -8,6 +8,7 @@ export interface Data {
fontSize: number fontSize: number
isDarkMode: boolean isDarkMode: boolean
isCodeOpen: boolean isCodeOpen: boolean
isTestMode: boolean
isDebugOpen: boolean isDebugOpen: boolean
isDebugMode: boolean isDebugMode: boolean
isStyleOpen: boolean isStyleOpen: boolean
@ -66,7 +67,7 @@ export interface Page {
export interface PageState { export interface PageState {
id: string id: string
selectedIds: Set<string> selectedIds: string[]
camera: { camera: {
point: number[] point: number[]
zoom: number zoom: number

View file

@ -1,4 +1,4 @@
import { clamp, deepClone, getCommonBounds, setToArray } from 'utils' import { clamp, deepClone, getCommonBounds } from 'utils'
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
import vec from './vec' import vec from './vec'
import { import {
@ -98,7 +98,7 @@ export default class StateUtils {
*/ */
static getSelectedShapes(data: Data): Shape[] { static getSelectedShapes(data: Data): Shape[] {
const page = this.getPage(data) const page = this.getPage(data)
const ids = setToArray(this.getSelectedIds(data)) const ids = this.getSelectedIds(data)
return ids.map((id) => page.shapes[id]) return ids.map((id) => page.shapes[id])
} }
@ -306,12 +306,12 @@ export default class StateUtils {
] ]
} }
static getSelectedIds(data: Data): Set<string> { static getSelectedIds(data: Data): string[] {
return data.pageStates[data.currentPageId].selectedIds return data.pageStates[data.currentPageId].selectedIds
} }
static setSelectedIds(data: Data, ids: string[]): Set<string> { static setSelectedIds(data: Data, ids: string[]): string[] {
data.pageStates[data.currentPageId].selectedIds = new Set(ids) data.pageStates[data.currentPageId].selectedIds = [...ids]
return data.pageStates[data.currentPageId].selectedIds return data.pageStates[data.currentPageId].selectedIds
} }
@ -347,7 +347,7 @@ export default class StateUtils {
>(data: Data, fn?: F): (Shape | K)[] { >(data: Data, fn?: F): (Shape | K)[] {
const page = this.getPage(data) const page = this.getPage(data)
const copies = setToArray(this.getSelectedIds(data)) const copies = this.getSelectedIds(data)
.flatMap((id) => .flatMap((id) =>
this.getDocumentBranch(data, id).map((id) => page.shapes[id]) this.getDocumentBranch(data, id).map((id) => page.shapes[id])
) )

View file

@ -1572,14 +1572,6 @@ export function uniqueArray<T extends string | number>(...items: T[]): T[] {
return Array.from(new Set(items).values()) return Array.from(new Set(items).values())
} }
/**
* Convert a set to an array.
* @param set
*/
export function setToArray<T>(set: Set<T>): T[] {
return Array.from(set.values())
}
/* -------------------------------------------------- */ /* -------------------------------------------------- */
/* Browser and DOM */ /* Browser and DOM */
/* -------------------------------------------------- */ /* -------------------------------------------------- */