diff --git a/__tests__/shapes/arrow.test.ts b/__tests__/shapes/arrow.test.ts index 8bd4ee470..82b1fee0d 100644 --- a/__tests__/shapes/arrow.test.ts +++ b/__tests__/shapes/arrow.test.ts @@ -1,17 +1,61 @@ +import { ArrowShape, ShapeType } from 'types' import TestState from '../test-utils' describe('arrow shape', () => { const tt = new TestState() - tt.resetDocumentState().send('SELECTED_ARROW_TOOL').save() + tt.resetDocumentState().save() - it('creates shape', () => { - // TODO - null - }) + describe('creating arrows', () => { + it('creates shape', () => { + tt.reset().restore().send('SELECTED_ARROW_TOOL') - it('cancels shape while creating', () => { - // TODO - null + expect(tt.state.isIn('arrow.creating')).toBe(true) + + tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas') + + const id = tt.getSortedPageShapeIds()[0] + + const shape = tt.getShape(id) + + tt.assertShapeType(id, ShapeType.Arrow) + + expect(shape.handles.start.point).toEqual([0, 0]) + expect(shape.handles.bend.point).toEqual([50.5, 50.5]) + expect(shape.handles.end.point).toEqual([101, 101]) + }) + + it('creates shapes when pointing a shape', () => { + tt.reset().restore().send('SELECTED_ARROW_TOOL').send('TOGGLED_TOOL_LOCK') + + tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas') + tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas') + + expect(tt.getSortedPageShapeIds()).toHaveLength(2) + }) + + it('creates shapes when shape locked', () => { + tt.reset() + .restore() + .createShape( + { + type: ShapeType.Rectangle, + point: [0, 0], + size: [100, 100], + childIndex: 1, + }, + 'rect1' + ) + .send('SELECTED_ARROW_TOOL') + + tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas') + + expect(tt.getSortedPageShapeIds()).toHaveLength(2) + }) + + it('cancels shape while creating', () => { + // TODO + null + }) }) it('moves shape', () => { diff --git a/__tests__/shapes/draw.test.ts b/__tests__/shapes/draw.test.ts new file mode 100644 index 000000000..a261344fc --- /dev/null +++ b/__tests__/shapes/draw.test.ts @@ -0,0 +1,101 @@ +import { ShapeType } from 'types' +import TestState from '../test-utils' + +describe('draw shape', () => { + const tt = new TestState() + tt.resetDocumentState().save() + + describe('creating draws', () => { + it('creates shape', () => { + tt.reset().restore().send('SELECTED_DRAW_TOOL') + + expect(tt.state.isIn('draw.creating')).toBe(true) + + tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas') + + const id = tt.getSortedPageShapeIds()[0] + + tt.assertShapeType(id, ShapeType.Draw) + }) + + it('creates shapes when pointing a shape', () => { + tt.reset().restore().send('SELECTED_DRAW_TOOL').send('TOGGLED_TOOL_LOCK') + + tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas') + tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas') + + expect(tt.getSortedPageShapeIds()).toHaveLength(2) + }) + + it('creates shapes when shape locked', () => { + tt.reset() + .restore() + .createShape( + { + type: ShapeType.Rectangle, + point: [0, 0], + size: [100, 100], + childIndex: 1, + }, + 'rect1' + ) + .send('SELECTED_DRAW_TOOL') + + tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas') + + expect(tt.getSortedPageShapeIds()).toHaveLength(2) + }) + + it('cancels shape while creating', () => { + // TODO + null + }) + }) + + it('moves shape', () => { + // TODO + null + }) + + it('rotates shape', () => { + // TODO + null + }) + + it('rotates shape in a group', () => { + // TODO + null + }) + + it('measures shape bounds', () => { + // TODO + null + }) + + it('measures shape rotated bounds', () => { + // TODO + null + }) + + it('transforms single shape', () => { + // TODO + null + }) + + it('transforms in a group', () => { + // TODO + null + }) + + /* -------------------- Specific -------------------- */ + + it('closes the shape when the start and end points are near enough', () => { + // TODO + null + }) + + it('remains closed after resizing up', () => { + // TODO + null + }) +}) diff --git a/__tests__/shapes/ellipse.test.ts b/__tests__/shapes/ellipse.test.ts new file mode 100644 index 000000000..9f4f2cd13 --- /dev/null +++ b/__tests__/shapes/ellipse.test.ts @@ -0,0 +1,104 @@ +import { ShapeType } from 'types' +import TestState from '../test-utils' + +describe('ellipse shape', () => { + const tt = new TestState() + tt.resetDocumentState().save() + + describe('creating ellipses', () => { + it('creates shape', () => { + tt.reset().restore().send('SELECTED_ELLIPSE_TOOL') + + expect(tt.state.isIn('ellipse.creating')).toBe(true) + + tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas') + + const id = tt.getSortedPageShapeIds()[0] + + tt.assertShapeType(id, ShapeType.Ellipse) + }) + + it('creates shapes when pointing a shape', () => { + tt.reset() + .restore() + .send('SELECTED_ELLIPSE_TOOL') + .send('TOGGLED_TOOL_LOCK') + + tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas') + tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas') + + expect(tt.getSortedPageShapeIds()).toHaveLength(2) + }) + + it('creates shapes when shape locked', () => { + tt.reset() + .restore() + .createShape( + { + type: ShapeType.Rectangle, + point: [0, 0], + size: [100, 100], + childIndex: 1, + }, + 'rect1' + ) + .send('SELECTED_ELLIPSE_TOOL') + + tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas') + + expect(tt.getSortedPageShapeIds()).toHaveLength(2) + }) + + it('cancels shape while creating', () => { + // TODO + null + }) + }) + + it('moves shape', () => { + // TODO + null + }) + + it('rotates shape', () => { + // TODO + null + }) + + it('rotates shape in a group', () => { + // TODO + null + }) + + it('measures shape bounds', () => { + // TODO + null + }) + + it('measures shape rotated bounds', () => { + // TODO + null + }) + + it('transforms single shape', () => { + // TODO + null + }) + + it('transforms in a group', () => { + // TODO + null + }) + + /* -------------------- Specific -------------------- */ + + it('creates aspect-ratio-locked shape with shift key', () => { + // TODO + null + }) + + it('resizes aspect-ratio-locked shape with shift key', () => { + // TODO + null + }) +}) diff --git a/__tests__/shapes/rectangle.test.ts b/__tests__/shapes/rectangle.test.ts new file mode 100644 index 000000000..ee331ca28 --- /dev/null +++ b/__tests__/shapes/rectangle.test.ts @@ -0,0 +1,104 @@ +import { ShapeType } from 'types' +import TestState from '../test-utils' + +describe('rectangle shape', () => { + const tt = new TestState() + tt.resetDocumentState().save() + + describe('creating rectangles', () => { + it('creates shape', () => { + tt.reset().restore().send('SELECTED_RECTANGLE_TOOL') + + expect(tt.state.isIn('rectangle.creating')).toBe(true) + + tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas') + + const id = tt.getSortedPageShapeIds()[0] + + tt.assertShapeType(id, ShapeType.Rectangle) + }) + + it('creates shapes when pointing a shape', () => { + tt.reset() + .restore() + .send('SELECTED_RECTANGLE_TOOL') + .send('TOGGLED_TOOL_LOCK') + + tt.startClick('canvas').movePointerBy([100, 100]).stopClick('canvas') + tt.startClick('canvas').movePointerBy([-200, 100]).stopClick('canvas') + + expect(tt.getSortedPageShapeIds()).toHaveLength(2) + }) + + it('creates shapes when shape locked', () => { + tt.reset() + .restore() + .createShape( + { + type: ShapeType.Rectangle, + point: [0, 0], + size: [100, 100], + childIndex: 1, + }, + 'rect1' + ) + .send('SELECTED_RECTANGLE_TOOL') + + tt.startClick('rect1').movePointerBy([100, 100]).stopClick('canvas') + + expect(tt.getSortedPageShapeIds()).toHaveLength(2) + }) + + it('cancels shape while creating', () => { + // TODO + null + }) + }) + + it('moves shape', () => { + // TODO + null + }) + + it('rotates shape', () => { + // TODO + null + }) + + it('rotates shape in a group', () => { + // TODO + null + }) + + it('measures shape bounds', () => { + // TODO + null + }) + + it('measures shape rotated bounds', () => { + // TODO + null + }) + + it('transforms single shape', () => { + // TODO + null + }) + + it('transforms in a group', () => { + // TODO + null + }) + + /* -------------------- Specific -------------------- */ + + it('creates aspect-ratio-locked shape with shift key', () => { + // TODO + null + }) + + it('resizes aspect-ratio-locked shape with shift key', () => { + // TODO + null + }) +}) diff --git a/__tests__/shapes/text.test.ts b/__tests__/shapes/text.test.ts new file mode 100644 index 000000000..f70bf3443 --- /dev/null +++ b/__tests__/shapes/text.test.ts @@ -0,0 +1,79 @@ +import TestState from '../test-utils' + +describe('arrow shape', () => { + const tt = new TestState() + tt.resetDocumentState() + + it('creates shape', () => { + tt.send('SELECTED_TEXT_TOOL') + + expect(tt.state.isIn('text.creating')).toBe(true) + + const id = tt.getSortedPageShapeIds()[0] + + tt.clickCanvas() + + expect(tt.state.isIn('editingShape')).toBe(true) + + tt.send('EDITED_SHAPE', { + id, + change: { text: 'Hello world' }, + }) + + tt.send('BLURRED_EDITING_SHAPE', { id: id }) + + expect(tt.state.isIn('selecting')).toBe(true) + }) + + it('cancels shape while creating', () => { + // TODO + null + }) + + it('moves shape', () => { + // TODO + null + }) + + it('rotates shape', () => { + // TODO + null + }) + + it('rotates shape in a group', () => { + // TODO + null + }) + + it('measures shape bounds', () => { + // TODO + null + }) + + it('measures shape rotated bounds', () => { + // TODO + null + }) + + it('transforms single shape', () => { + // TODO + null + }) + + it('transforms in a group', () => { + // TODO + null + }) + + /* -------------------- Specific -------------------- */ + + it('scales', () => { + // TODO + null + }) + + it('selects different text on tap while editing', () => { + // TODO + null + }) +}) diff --git a/__tests__/test-utils.ts b/__tests__/test-utils.ts index f80b4104e..38a86915e 100644 --- a/__tests__/test-utils.ts +++ b/__tests__/test-utils.ts @@ -106,7 +106,7 @@ class TestState { */ getSortedPageShapeIds(): string[] { return Object.values( - this.data.document.pages[this.data.currentParentId].shapes + this.data.document.pages[this.data.currentPageId].shapes ) .sort((a, b) => a.childIndex - b.childIndex) .map((shape) => shape.id) @@ -257,6 +257,14 @@ class TestState { const shape = tld.getShape(this.data, id) const [x, y] = shape ? vec.add(shape.point, [1, 1]) : [0, 0] + if (id === 'canvas') { + this.state.send( + 'POINTED_CANVAS', + inputs.pointerDown(TestState.point({ x, y, ...options }), id) + ) + return this + } + this.state.send( 'POINTED_SHAPE', inputs.pointerDown(TestState.point({ x, y, ...options }), id) diff --git a/__tests__/tools.test.ts b/__tests__/tools.test.ts new file mode 100644 index 000000000..b86a64d21 --- /dev/null +++ b/__tests__/tools.test.ts @@ -0,0 +1,38 @@ +import TestState from './test-utils' + +const TOOLS = [ + 'draw', + 'rectangle', + 'ellipse', + 'arrow', + 'text', + 'line', + 'ray', + 'dot', +] + +describe('when selecting tools', () => { + const tt = new TestState() + + TOOLS.forEach((tool) => { + it(`selects ${tool} tool`, () => { + tt.reset().send(`SELECTED_${tool.toUpperCase()}_TOOL`) + + expect(tt.data.activeTool).toBe(tool) + expect(tt.state.isIn(tool)).toBe(true) + }) + + TOOLS.forEach((otherTool) => { + if (otherTool === tool) return + + it(`selects ${tool} tool from ${otherTool} tool`, () => { + tt.reset() + .send(`SELECTED_${tool.toUpperCase()}_TOOL`) + .send(`SELECTED_${otherTool.toUpperCase()}_TOOL`) + + expect(tt.data.activeTool).toBe(otherTool) + expect(tt.state.isIn(otherTool)).toBe(true) + }) + }) + }) +}) diff --git a/components/code-panel/code-as-string.ts b/components/code-panel/code-as-string.ts deleted file mode 100644 index 7882b1c9d..000000000 --- a/components/code-panel/code-as-string.ts +++ /dev/null @@ -1,9 +0,0 @@ -// This is the code library. - -export default ` -class Circle { - greet(): string { - return "Hello!" - } -} -` diff --git a/components/code-panel/example-code.ts b/components/code-panel/example-code.ts deleted file mode 100644 index 60db24f0e..000000000 --- a/components/code-panel/example-code.ts +++ /dev/null @@ -1,8 +0,0 @@ -export default `new Circle({ - point: [200, 200], -}) - -new Rectangle({ - point: [400, 300], -}) -` diff --git a/components/code-panel/types-import.ts b/components/code-panel/types-import.ts index 4d848d921..63d7eb7e1 100644 --- a/components/code-panel/types-import.ts +++ b/components/code-panel/types-import.ts @@ -62,6 +62,8 @@ enum FontSize { ExtraLarge = 'ExtraLarge', } +type Theme = 'dark' | 'light' + type ShapeStyles = { color: ColorStyle size: SizeStyle @@ -214,6 +216,16 @@ interface CodeResult { error: CodeError } +interface ShapeTreeNode { + shape: Shape + children: ShapeTreeNode[] + isEditing: boolean + isHovered: boolean + isSelected: boolean + isDarkMode: boolean + isCurrentParent: boolean +} + /* -------------------------------------------------- */ /* Editor UI */ /* -------------------------------------------------- */ @@ -545,8 +557,12 @@ interface ShapeUtility { render( this: ShapeUtility, shape: K, - info: { - isEditing: boolean + info?: { + isEditing?: boolean + isHovered?: boolean + isSelected?: boolean + isCurrentParent?: boolean + isDarkMode?: boolean ref?: React.MutableRefObject } ): JSX.Element @@ -636,6 +652,8 @@ enum FontSize { ExtraLarge = 'ExtraLarge', } +type Theme = 'dark' | 'light' + type ShapeStyles = { color: ColorStyle size: SizeStyle @@ -788,6 +806,16 @@ interface CodeResult { error: CodeError } +interface ShapeTreeNode { + shape: Shape + children: ShapeTreeNode[] + isEditing: boolean + isHovered: boolean + isSelected: boolean + isDarkMode: boolean + isCurrentParent: boolean +} + /* -------------------------------------------------- */ /* Editor UI */ /* -------------------------------------------------- */ @@ -1119,8 +1147,12 @@ interface ShapeUtility { render( this: ShapeUtility, shape: K, - info: { - isEditing: boolean + info?: { + isEditing?: boolean + isHovered?: boolean + isSelected?: boolean + isCurrentParent?: boolean + isDarkMode?: boolean ref?: React.MutableRefObject } ): JSX.Element @@ -1840,6 +1872,14 @@ type RequiredKeys = { return Array.from(new Set(items).values()) } + /** + * Convert a set to an array. + * @param set + */ + static setToArray(set: Set): T[] { + return Array.from(set.values()) + } + /** * Get the outer of between a circle and a point. * @param C The circle's center. diff --git a/components/status-bar.tsx b/components/status-bar.tsx index 9571baf88..913e03dac 100644 --- a/components/status-bar.tsx +++ b/components/status-bar.tsx @@ -9,10 +9,13 @@ export default function StatusBar(): JSX.Element { const shapesInView = state.values.shapesToRender.length - const active = local.active.slice(1).map((s) => { - const states = s.split('.') - return states[states.length - 1] - }) + const active = local.active + .slice(1) + .map((s) => { + const states = s.split('.') + return states[states.length - 1] + }) + .join(' | ') const log = local.log[0] @@ -21,7 +24,7 @@ export default function StatusBar(): JSX.Element { return (
- {active.join(' | ')} - {log} + {active} - {log}
{shapesInView || '0'} Shapes
diff --git a/components/style-panel/quick-fill-select.tsx b/components/style-panel/quick-fill-select.tsx index 19650750f..6944b4c34 100644 --- a/components/style-panel/quick-fill-select.tsx +++ b/components/style-panel/quick-fill-select.tsx @@ -21,7 +21,7 @@ export default function IsFilledPicker(): JSX.Element { return ( selectedShapes.length === 0 || - selectedShapes.every((shape) => getShapeUtils(shape).canStyleFill) + selectedShapes.some((shape) => getShapeUtils(shape).canStyleFill) ) }) diff --git a/components/tools-panel/shared.tsx b/components/tools-panel/shared.tsx index efae54048..f7f2e17d3 100644 --- a/components/tools-panel/shared.tsx +++ b/components/tools-panel/shared.tsx @@ -135,7 +135,7 @@ export function PrimaryButton({ children, }: PrimaryToolButtonProps): JSX.Element { return ( - + { - if (isMobile().apple) { + if (isMobile()) { document.addEventListener('focusout', handleFocusOut) return () => { diff --git a/state/state.ts b/state/state.ts index 96c0c4f39..f60ab5a86 100644 --- a/state/state.ts +++ b/state/state.ts @@ -93,8 +93,8 @@ const initialData: Data = { const draw = new Draw({ points: [ ...Utils.getPointsBetween([0, 0], [20, 50]), - ...Utils.getPointsBetween([20, 50], [100, 20], 3), - ...Utils.getPointsBetween([100, 20], [100, 100], 10), + ...Utils.getPointsBetween([20, 50], [100, 20], { steps: 3 }), + ...Utils.getPointsBetween([100, 20], [100, 100], { steps: 10 }), [100, 100], ], }) @@ -884,9 +884,9 @@ const state = createState({ }, arrow: { onEnter: 'setActiveToolArrow', - initial: 'idle', + initial: 'creating', states: { - idle: { + creating: { on: { CANCELLED: { to: 'selecting' }, POINTED_SHAPE: { @@ -937,6 +937,9 @@ const state = createState({ POINTED_CANVAS: { to: 'ellipse.editing', }, + POINTED_SHAPE: { + to: 'ellipse.editing', + }, }, }, editing: {