Fix double undos, cleans up utils

This commit is contained in:
Steve Ruiz 2021-06-24 13:34:43 +01:00
parent 8271e6d431
commit bdafae3db6
53 changed files with 1676 additions and 1515 deletions

31
__tests__/delete.test.ts Normal file
View file

@ -0,0 +1,31 @@
import state from 'state'
import inputs from 'state/inputs'
import { getShape } from 'utils'
import { idsAreSelected, point, rectangleId } from './test-utils'
import * as json from './__mocks__/document.json'
state.reset()
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })
describe('selection', () => {
it('deletes a shape and undoes the delete', () => {
state
.send('CANCELED')
.send('POINTED_SHAPE', inputs.pointerDown(point(), rectangleId))
.send('STOPPED_POINTING', inputs.pointerUp(point(), rectangleId))
.send('DELETED')
expect(getShape(state.data, rectangleId)).toBe(undefined)
expect(idsAreSelected(state.data, [])).toBe(true)
state.send('UNDO')
expect(getShape(state.data, rectangleId)).toBeTruthy()
expect(idsAreSelected(state.data, [rectangleId])).toBe(true)
state.send('REDO')
expect(getShape(state.data, rectangleId)).toBe(undefined)
expect(idsAreSelected(state.data, [])).toBe(true)
})
})

View file

@ -1,11 +1,8 @@
import state from 'state' import state from 'state'
import inputs from 'state/inputs' import inputs from 'state/inputs'
import { idsAreSelected, point } from './test-utils' import { idsAreSelected, point, rectangleId, arrowId } from './test-utils'
import * as json from './__mocks__/document.json' import * as json from './__mocks__/document.json'
const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
// Mount the state and load the test file from json // Mount the state and load the test file from json
state.reset() state.reset()
state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) }) state.send('MOUNTED').send('LOADED_FROM_FILE', { json: JSON.stringify(json) })

View file

@ -1,6 +1,9 @@
import { Data } from 'types' import { Data } from 'types'
import { getSelectedIds } from 'utils' import { getSelectedIds } from 'utils'
export const rectangleId = '1f6c251c-e12e-40b4-8dd2-c1847d80b72f'
export const arrowId = '5ca167d7-54de-47c9-aa8f-86affa25e44d'
interface PointerOptions { interface PointerOptions {
id?: string id?: string
x?: number x?: number

View file

@ -1,8 +1,13 @@
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
import { useSelector } from 'state' import { useSelector } from 'state'
import { Bounds, PageState } from 'types' import { Bounds, PageState } from 'types'
import { boundsCollide, boundsContain } from 'utils/bounds' import {
import { deepCompareArrays, getPage, getViewport } from 'utils' deepCompareArrays,
getPage,
getViewport,
boundsCollide,
boundsContain,
} from 'utils'
import Shape from './shape' import Shape from './shape'
/* /*

View file

@ -94,11 +94,11 @@ class Clipboard {
try { try {
navigator.clipboard.writeText(svgString) navigator.clipboard.writeText(svgString)
} catch (e) { } catch (e) {
Clipboard.copyStringToClipboard(svgString) this.copyStringToClipboard(svgString)
} }
} }
static copyStringToClipboard(string: string) { copyStringToClipboard = (string: string) => {
let result: boolean | null let result: boolean | null
const textarea = document.createElement('textarea') const textarea = document.createElement('textarea')

View file

@ -1,17 +1,10 @@
import { Bounds } from 'types' import { Bounds } from 'types'
import { ease } from 'utils'
import vec from 'utils/vec' import vec from 'utils/vec'
/** /**
* ## Utils * ## Utils
*/ */
export default class Utils { export default class Utils {
static pointsBetween(a: number[], b: number[], steps = 6): number[][] {
return Array.from(Array(steps))
.map((_, i) => ease(i / steps))
.map((t) => [...vec.lrp(a, b, t), (1 - t) / 2])
}
static getRayRayIntersection( static getRayRayIntersection(
p0: number[], p0: number[],
n0: number[], n0: number[],

View file

@ -5,7 +5,6 @@ import { getCommonBounds, getPage, getSelectedShapes } from 'utils'
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
export default function alignCommand(data: Data, type: AlignType): void { export default function alignCommand(data: Data, type: AlignType): void {
const { currentPageId } = data
const selectedShapes = getSelectedShapes(data) const selectedShapes = getSelectedShapes(data)
const entries = selectedShapes.map( const entries = selectedShapes.map(
(shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const (shape) => [shape.id, getShapeUtils(shape).getBounds(shape)] as const
@ -21,7 +20,7 @@ export default function alignCommand(data: Data, type: AlignType): void {
name: 'aligned', name: 'aligned',
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
switch (type) { switch (type) {
case AlignType.Top: { case AlignType.Top: {
@ -87,7 +86,7 @@ export default function alignCommand(data: Data, type: AlignType): void {
} }
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in boundsForShapes) { for (const id in boundsForShapes) {
const shape = shapes[id] const shape = shapes[id]
const initialBounds = boundsForShapes[id] const initialBounds = boundsForShapes[id]

View file

@ -18,9 +18,9 @@ export default function arrowCommand(
do(data, isInitial) { do(data, isInitial) {
if (isInitial) return if (isInitial) return
const { initialShape, currentPageId } = after const { initialShape } = after
const page = getPage(data, currentPageId) const page = getPage(data)
page.shapes[initialShape.id] = initialShape page.shapes[initialShape.id] = initialShape
@ -31,8 +31,8 @@ export default function arrowCommand(
data.pointedId = undefined data.pointedId = undefined
}, },
undo(data) { undo(data) {
const { initialShape, currentPageId } = before const { initialShape } = before
const shapes = getPage(data, currentPageId).shapes const shapes = getPage(data).shapes
delete shapes[initialShape.id] delete shapes[initialShape.id]

View file

@ -2,7 +2,6 @@ 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'
import { current } from 'immer'
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 {
@ -40,7 +39,7 @@ export default function createPage(data: Data, goToPage = true): void {
} }
function getSnapshot(data: Data) { function getSnapshot(data: Data) {
const { currentPageId } = current(data) const { currentPageId } = data
const pages = Object.values(data.document.pages) const pages = Object.values(data.document.pages)
const unchanged = pages.filter((page) => page.name.startsWith('Page ')) const unchanged = pages.filter((page) => page.name.startsWith('Page '))

View file

@ -1,8 +1,8 @@
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 { current } from 'immer'
import storage from 'state/storage' import storage from 'state/storage'
import { deepClone, getPage, getPageState } from 'utils'
export default function deletePage(data: Data, pageId: string): void { export default function deletePage(data: Data, pageId: string): void {
const snapshot = getSnapshot(data, pageId) const snapshot = getSnapshot(data, pageId)
@ -29,23 +29,17 @@ export default function deletePage(data: Data, pageId: string): void {
} }
function getSnapshot(data: Data, pageId: string) { function getSnapshot(data: Data, pageId: string) {
const cData = current(data) const { currentPageId, document } = data
const { currentPageId, document } = cData
const page = document.pages[pageId] const page = deepClone(getPage(data))
const pageState = cData.pageStates[pageId]
const isCurrent = currentPageId === pageId const pageState = deepClone(getPageState(data))
// const nextIndex = isCurrent const isCurrent = data.currentPageId === pageId
// ? page.childIndex === 0
// ? 1
// : page.childIndex - 1
// : document.pages[currentPageId].childIndex
const nextPageId = isCurrent const nextPageId = isCurrent
? Object.values(document.pages).filter((page) => page.id !== pageId)[0]?.id // TODO: should be at nextIndex ? Object.values(document.pages).filter((page) => page.id !== pageId)[0]?.id // TODO: should be at nextIndex
: cData.currentPageId : currentPageId
return { return {
nextPageId, nextPageId,

View file

@ -2,28 +2,26 @@ import Command from './command'
import history from '../history' import history from '../history'
import { Data } from 'types' import { Data } from 'types'
import { import {
deepClone,
getDocumentBranch, getDocumentBranch,
getPage, getPage,
getSelectedShapes, getSelectedShapes,
setSelectedIds, setSelectedIds,
} from 'utils' } from 'utils'
import { current } from 'immer'
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
export default function deleteSelected(data: Data): void { export default function deleteSelected(data: Data): void {
const { currentPageId } = data
const selectedShapes = getSelectedShapes(data) const selectedShapes = getSelectedShapes(data)
const selectedIdsArr = selectedShapes const selectedIdsArr = selectedShapes
.filter((shape) => !shape.isLocked) .filter((shape) => !shape.isLocked)
.map((shape) => shape.id) .map((shape) => shape.id)
const page = getPage(current(data)) const page = getPage(data)
const childrenToDelete = selectedIdsArr const childrenToDelete = selectedIdsArr
.flatMap((id) => getDocumentBranch(data, id)) .flatMap((id) => getDocumentBranch(data, id))
.map((id) => page.shapes[id]) .map((id) => deepClone(page.shapes[id]))
const remainingIds = selectedShapes const remainingIds = selectedShapes
.filter((shape) => shape.isLocked) .filter((shape) => shape.isLocked)
@ -36,7 +34,7 @@ export default function deleteSelected(data: Data): void {
category: 'canvas', category: 'canvas',
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const page = getPage(data, currentPageId) const page = getPage(data)
for (const id of selectedIdsArr) { for (const id of selectedIdsArr) {
const shape = page.shapes[id] const shape = page.shapes[id]
@ -67,7 +65,7 @@ export default function deleteSelected(data: Data): void {
setSelectedIds(data, remainingIds) setSelectedIds(data, remainingIds)
}, },
undo(data) { undo(data) {
const page = getPage(data, currentPageId) const page = getPage(data)
for (const shape of childrenToDelete) { for (const shape of childrenToDelete) {
page.shapes[shape.id] = shape page.shapes[shape.id] = shape

View file

@ -24,7 +24,7 @@ export default function directCommand(
} }
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, before.currentPageId) const { shapes } = getPage(data)
for (const { id, direction } of after.shapes) { for (const { id, direction } of after.shapes) {
const shape = shapes[id] as RayShape | LineShape const shape = shapes[id] as RayShape | LineShape

View file

@ -13,8 +13,6 @@ export default function distributeCommand(
data: Data, data: Data,
type: DistributeType type: DistributeType
): void { ): void {
const { currentPageId } = data
const selectedShapes = getSelectedShapes(data).filter( const selectedShapes = getSelectedShapes(data).filter(
(shape) => !shape.isLocked (shape) => !shape.isLocked
) )
@ -40,7 +38,7 @@ export default function distributeCommand(
name: 'distribute_shapes', name: 'distribute_shapes',
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
const len = entries.length const len = entries.length
switch (type) { switch (type) {
@ -132,7 +130,7 @@ export default function distributeCommand(
} }
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in boundsForShapes) { for (const id in boundsForShapes) {
const shape = shapes[id] const shape = shapes[id]
const initialBounds = boundsForShapes[id] const initialBounds = boundsForShapes[id]

View file

@ -1,11 +1,10 @@
import Command from './command' import Command from './command'
import history from '../history' import history from '../history'
import { Data, DrawShape } from 'types' import { Data, DrawShape } from 'types'
import { getPage, setSelectedIds } from 'utils' import { deepClone, getPage, getShape, setSelectedIds } from 'utils'
import { current } from 'immer'
export default function drawCommand(data: Data, id: string): void { export default function drawCommand(data: Data, id: string): void {
const restoreShape = getPage(current(data)).shapes[id] as DrawShape const restoreShape = deepClone(getShape(data, id)) as DrawShape
history.execute( history.execute(
data, data,

View file

@ -2,18 +2,18 @@ import Command from './command'
import history from '../history' import history from '../history'
import { Data } from 'types' import { Data } from 'types'
import { import {
deepClone,
getCurrentCamera, getCurrentCamera,
getPage, getPage,
getSelectedShapes, getSelectedShapes,
setSelectedIds, setSelectedIds,
} from 'utils' } from 'utils'
import { uniqueId } from 'utils' import { uniqueId } from 'utils'
import { current } from 'immer'
import vec from 'utils/vec' import vec from 'utils/vec'
export default function duplicateCommand(data: Data): void { export default function duplicateCommand(data: Data): void {
const { currentPageId } = data const selectedShapes = getSelectedShapes(data).map(deepClone)
const selectedShapes = getSelectedShapes(current(data))
const duplicates = selectedShapes.map((shape) => ({ const duplicates = selectedShapes.map((shape) => ({
...shape, ...shape,
id: uniqueId(), id: uniqueId(),
@ -28,7 +28,7 @@ export default function duplicateCommand(data: Data): void {
category: 'canvas', category: 'canvas',
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const duplicate of duplicates) { for (const duplicate of duplicates) {
shapes[duplicate.id] = duplicate shapes[duplicate.id] = duplicate
@ -40,7 +40,7 @@ export default function duplicateCommand(data: Data): void {
) )
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const duplicate of duplicates) { for (const duplicate of duplicates) {
delete shapes[duplicate.id] delete shapes[duplicate.id]

View file

@ -16,9 +16,9 @@ export default function editCommand(
name: 'edit_shape', name: 'edit_shape',
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { initialShape, currentPageId } = after const { initialShape } = after
const page = getPage(data, currentPageId) const page = getPage(data)
page.shapes[initialShape.id] = initialShape page.shapes[initialShape.id] = initialShape
@ -29,9 +29,9 @@ export default function editCommand(
} }
}, },
undo(data) { undo(data) {
const { initialShape, currentPageId } = before const { initialShape } = before
const page = getPage(data, currentPageId) const page = getPage(data)
page.shapes[initialShape.id] = initialShape page.shapes[initialShape.id] = initialShape
}, },

View file

@ -1,34 +1,15 @@
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 { current } from 'immer' import { deepClone, getPage, getShapes, setSelectedIds } from 'utils'
import { getPage, setSelectedIds } from 'utils'
export default function generateCommand( export default function generateCommand(
data: Data, data: Data,
currentPageId: string,
generatedShapes: Shape[] generatedShapes: Shape[]
): void { ): void {
const cData = current(data) const initialShapes = getShapes(data)
const page = getPage(cData) .filter((shape) => shape.isGenerated)
.map(deepClone)
const currentShapes = page.shapes
const prevGeneratedShapes = Object.values(currentShapes).filter(
(shape) => shape.isGenerated
)
// Remove previous generated shapes
for (const id in currentShapes) {
if (currentShapes[id].isGenerated) {
delete currentShapes[id]
}
}
// Add new ones
for (const shape of generatedShapes) {
currentShapes[shape.id] = shape
}
history.execute( history.execute(
data, data,
@ -37,35 +18,15 @@ export default function generateCommand(
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { shapes } = getPage(data) const { shapes } = getPage(data)
initialShapes.forEach((shape) => delete shapes[shape.id])
generatedShapes.forEach((shape) => (shapes[shape.id] = shape))
setSelectedIds(data, []) setSelectedIds(data, [])
// Remove previous generated shapes
for (const id in shapes) {
if (shapes[id].isGenerated) {
delete shapes[id]
}
}
// Add new generated shapes
for (const shape of generatedShapes) {
shapes[shape.id] = shape
}
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data) const { shapes } = getPage(data)
generatedShapes.forEach((shape) => delete shapes[shape.id])
// Remove generated shapes initialShapes.forEach((shape) => (shapes[shape.id] = shape))
for (const id in shapes) { setSelectedIds(data, [])
if (shapes[id].isGenerated) {
delete shapes[id]
}
}
// Restore previous generated shapes
for (const shape of prevGeneratedShapes) {
shapes[shape.id] = shape
}
}, },
}) })
) )

View file

@ -82,7 +82,7 @@ export default function groupCommand(data: Data): void {
category: 'canvas', category: 'canvas',
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
// Create the new group // Create the new group
shapes[newGroupShape.id] = newGroupShape shapes[newGroupShape.id] = newGroupShape
@ -118,7 +118,7 @@ export default function groupCommand(data: Data): void {
setSelectedIds(data, [newGroupShape.id]) setSelectedIds(data, [newGroupShape.id])
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
const group = shapes[newGroupShape.id] const group = shapes[newGroupShape.id]

View file

@ -16,9 +16,9 @@ export default function handleCommand(
name: 'moved_handle', name: 'moved_handle',
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { initialShape, currentPageId } = after const { initialShape } = after
const page = getPage(data, currentPageId) const page = getPage(data)
const shape = page.shapes[initialShape.id] const shape = page.shapes[initialShape.id]
getShapeUtils(shape) getShapeUtils(shape)
@ -26,9 +26,9 @@ export default function handleCommand(
.onSessionComplete(shape) .onSessionComplete(shape)
}, },
undo(data) { undo(data) {
const { initialShape, currentPageId } = before const { initialShape } = before
const page = getPage(data, currentPageId) const page = getPage(data)
page.shapes[initialShape.id] = initialShape page.shapes[initialShape.id] = initialShape
}, },
}) })

View file

@ -39,7 +39,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
const fromPageId = oldPageId const fromPageId = oldPageId
const toPageId = newPageId const toPageId = newPageId
const fromPage = getPage(data, fromPageId) const fromPage = getPage(data)
// Get all of the selected shapes and their descendents // Get all of the selected shapes and their descendents
const shapesToMove = idsToMove.map((id) => fromPage.shapes[id]) const shapesToMove = idsToMove.map((id) => fromPage.shapes[id])
@ -65,7 +65,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
getPageState(data, fromPageId).selectedIds.clear() getPageState(data).selectedIds.clear()
// Save the "from" page // Save the "from" page
storage.savePage(data, data.document.id, fromPageId) storage.savePage(data, data.document.id, fromPageId)
@ -74,7 +74,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
storage.loadPage(data, toPageId) storage.loadPage(data, toPageId)
// The page we're moving the shapes to // The page we're moving the shapes to
const toPage = getPage(data, toPageId) const toPage = getPage(data)
// Add all of the selected shapes to the "from" page. // Add all of the selected shapes to the "from" page.
shapesToMove.forEach((shape) => { shapesToMove.forEach((shape) => {
@ -89,7 +89,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
getPageState(data, toPageId).selectedIds = new Set(selectedIds) getPageState(data).selectedIds = new Set(selectedIds)
// Move to the new page // Move to the new page
data.currentPageId = toPageId data.currentPageId = toPageId
@ -98,7 +98,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
const fromPageId = newPageId const fromPageId = newPageId
const toPageId = oldPageId const toPageId = oldPageId
const fromPage = getPage(data, fromPageId) const fromPage = getPage(data)
const shapesToMove = idsToMove.map((id) => fromPage.shapes[id]) const shapesToMove = idsToMove.map((id) => fromPage.shapes[id])
@ -119,13 +119,13 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
delete fromPage.shapes[shape.id] delete fromPage.shapes[shape.id]
}) })
getPageState(data, fromPageId).selectedIds.clear() getPageState(data).selectedIds.clear()
storage.savePage(data, data.document.id, fromPageId) storage.savePage(data, data.document.id, fromPageId)
storage.loadPage(data, toPageId) storage.loadPage(data, toPageId)
const toPage = getPage(data, toPageId) const toPage = getPage(data)
shapesToMove.forEach((shape) => { shapesToMove.forEach((shape) => {
toPage.shapes[shape.id] = shape toPage.shapes[shape.id] = shape
@ -144,7 +144,7 @@ export default function moveToPageCommand(data: Data, newPageId: string): void {
} }
}) })
getPageState(data, toPageId).selectedIds = new Set(selectedIds) getPageState(data).selectedIds = new Set(selectedIds)
data.currentPageId = toPageId data.currentPageId = toPageId
}, },

View file

@ -11,8 +11,6 @@ import {
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 { currentPageId } = data
const page = getPage(data) const page = getPage(data)
const selectedIds = setToArray(getSelectedIds(data)) const selectedIds = setToArray(getSelectedIds(data))
@ -28,7 +26,7 @@ export default function moveCommand(data: Data, type: MoveType): void {
category: 'canvas', category: 'canvas',
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const page = getPage(data, currentPageId) const page = getPage(data)
const shapes = selectedIds.map((id) => page.shapes[id]) const shapes = selectedIds.map((id) => page.shapes[id])

View file

@ -6,7 +6,6 @@ import { getShapeUtils } from 'state/shape-utils'
import vec from 'utils/vec' import vec from 'utils/vec'
export default function nudgeCommand(data: Data, delta: number[]): void { export default function nudgeCommand(data: Data, delta: number[]): void {
const { currentPageId } = data
const selectedShapes = getSelectedShapes(data) const selectedShapes = getSelectedShapes(data)
const shapeBounds = Object.fromEntries( const shapeBounds = Object.fromEntries(
selectedShapes.map( selectedShapes.map(
@ -20,7 +19,7 @@ export default function nudgeCommand(data: Data, delta: number[]): void {
name: 'nudge_shapes', name: 'nudge_shapes',
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in shapeBounds) { for (const id in shapeBounds) {
const shape = shapes[id] const shape = shapes[id]
@ -32,7 +31,7 @@ export default function nudgeCommand(data: Data, delta: number[]): void {
} }
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in shapeBounds) { for (const id in shapeBounds) {
const shape = shapes[id] const shape = shapes[id]

View file

@ -15,8 +15,6 @@ import { getShapeUtils } from 'state/shape-utils'
import state from 'state/state' import state from 'state/state'
export default function pasteCommand(data: Data, initialShapes: Shape[]): void { export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
const { currentPageId } = data
const center = screenToWorld( const center = screenToWorld(
[window.innerWidth / 2, window.innerHeight / 2], [window.innerWidth / 2, window.innerHeight / 2],
data data
@ -43,7 +41,7 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
category: 'canvas', category: 'canvas',
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
let childIndex = let childIndex =
(state.values.currentShapes[state.values.currentShapes.length - 1] (state.values.currentShapes[state.values.currentShapes.length - 1]
@ -67,7 +65,7 @@ export default function pasteCommand(data: Data, initialShapes: Shape[]): void {
setSelectedIds(data, Object.values(newIdMap)) setSelectedIds(data, Object.values(newIdMap))
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
Object.values(newIdMap).forEach((id) => delete shapes[id]) Object.values(newIdMap).forEach((id) => delete shapes[id])

View file

@ -13,7 +13,7 @@ import { getShapeUtils } from 'state/shape-utils'
const PI2 = Math.PI * 2 const PI2 = Math.PI * 2
export default function rotateCcwCommand(data: Data): void { export default function rotateCcwCommand(data: Data): void {
const { currentPageId, boundsRotation } = data const { boundsRotation } = data
const page = getPage(data) const page = getPage(data)
@ -63,7 +63,7 @@ export default function rotateCcwCommand(data: Data): void {
name: 'rotate_ccw', name: 'rotate_ccw',
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in nextShapes) { for (const id in nextShapes) {
const shape = shapes[id] const shape = shapes[id]
@ -77,7 +77,7 @@ export default function rotateCcwCommand(data: Data): void {
data.boundsRotation = nextboundsRotation data.boundsRotation = nextboundsRotation
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in initialShapes) { for (const id in initialShapes) {
const { point, rotation } = initialShapes[id] const { point, rotation } = initialShapes[id]

View file

@ -29,7 +29,7 @@ export default function rotateCommand(
data.boundsRotation = after.boundsRotation data.boundsRotation = after.boundsRotation
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, before.currentPageId) const { shapes } = getPage(data)
for (const { id, point, rotation } of before.initialShapes) { for (const { id, point, rotation } of before.initialShapes) {
const shape = shapes[id] const shape = shapes[id]

View file

@ -5,8 +5,6 @@ import { deepClone, getCommonBounds, getPage, getSelectedShapes } from 'utils'
import { getShapeUtils } from 'state/shape-utils' import { getShapeUtils } from 'state/shape-utils'
export default function stretchCommand(data: Data, type: StretchType): void { export default function stretchCommand(data: Data, type: StretchType): void {
const { currentPageId } = data
const initialShapes = getSelectedShapes(data).map((shape) => deepClone(shape)) const initialShapes = getSelectedShapes(data).map((shape) => deepClone(shape))
const snapshot = Object.fromEntries( const snapshot = Object.fromEntries(
@ -29,7 +27,7 @@ export default function stretchCommand(data: Data, type: StretchType): void {
name: 'stretched_shapes', name: 'stretched_shapes',
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
switch (type) { switch (type) {
case StretchType.Horizontal: { case StretchType.Horizontal: {
@ -77,7 +75,7 @@ export default function stretchCommand(data: Data, type: StretchType): void {
} }
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
initialShapes.forEach((shape) => (shapes[shape.id] = shape)) initialShapes.forEach((shape) => (shapes[shape.id] = shape))
}, },
}) })

View file

@ -11,7 +11,6 @@ export default function styleCommand(
): void { ): void {
const cData = current(data) const cData = current(data)
const page = getPage(cData) const page = getPage(cData)
const { currentPageId } = cData
const selectedIds = setToArray(getSelectedIds(data)) const selectedIds = setToArray(getSelectedIds(data))
@ -26,7 +25,7 @@ export default function styleCommand(
category: 'canvas', category: 'canvas',
manualSelection: true, manualSelection: true,
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const { id } of shapesToStyle) { for (const { id } of shapesToStyle) {
const shape = shapes[id] const shape = shapes[id]
@ -34,7 +33,7 @@ export default function styleCommand(
} }
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const { id, style } of shapesToStyle) { for (const { id, style } of shapesToStyle) {
const shape = shapes[id] const shape = shapes[id]

View file

@ -9,7 +9,6 @@ export default function toggleCommand(
data: Data, data: Data,
prop: PropsOfType<Shape> prop: PropsOfType<Shape>
): void { ): void {
const { currentPageId } = data
const selectedShapes = getSelectedShapes(data) const selectedShapes = getSelectedShapes(data)
const isAllToggled = selectedShapes.every((shape) => shape[prop]) const isAllToggled = selectedShapes.every((shape) => shape[prop])
const initialShapes = Object.fromEntries( const initialShapes = Object.fromEntries(
@ -22,7 +21,7 @@ export default function toggleCommand(
name: 'toggle_prop', name: 'toggle_prop',
category: 'canvas', category: 'canvas',
do(data) { do(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in initialShapes) { for (const id in initialShapes) {
const shape = shapes[id] const shape = shapes[id]
@ -34,7 +33,7 @@ export default function toggleCommand(
} }
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in initialShapes) { for (const id in initialShapes) {
const shape = shapes[id] const shape = shapes[id]

View file

@ -11,7 +11,7 @@ export default function transformSingleCommand(
after: TransformSingleSnapshot, after: TransformSingleSnapshot,
isCreating: boolean isCreating: boolean
): void { ): void {
const shape = current(getPage(data, after.currentPageId).shapes[after.id]) const shape = current(getPage(data).shapes[after.id])
history.execute( history.execute(
data, data,
@ -22,7 +22,7 @@ export default function transformSingleCommand(
do(data) { do(data) {
const { id } = after const { id } = after
const { shapes } = getPage(data, after.currentPageId) const { shapes } = getPage(data)
setSelectedIds(data, [id]) setSelectedIds(data, [id])
@ -33,7 +33,7 @@ export default function transformSingleCommand(
undo(data) { undo(data) {
const { id, initialShape } = before const { id, initialShape } = before
const { shapes } = getPage(data, before.currentPageId) const { shapes } = getPage(data)
if (isCreating) { if (isCreating) {
setSelectedIds(data, []) setSelectedIds(data, [])

View file

@ -25,8 +25,8 @@ export default function translateCommand(
do(data, initial) { do(data, initial) {
if (initial) return if (initial) return
const { initialShapes, currentPageId } = after const { initialShapes } = after
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
// Restore clones to document // Restore clones to document
if (isCloning) { if (isCloning) {
@ -66,8 +66,8 @@ export default function translateCommand(
) )
}, },
undo(data) { undo(data) {
const { initialShapes, clones, currentPageId, initialParents } = before const { initialShapes, clones, initialParents } = before
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
// Move shapes back to where they started // Move shapes back to where they started
for (const { id, point } of initialShapes) { for (const { id, point } of initialShapes) {

View file

@ -68,7 +68,7 @@ export default function ungroupCommand(data: Data): void {
} }
}, },
undo(data) { undo(data) {
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
selectedGroups.forEach((group) => { selectedGroups.forEach((group) => {
shapes[group.id] = group shapes[group.id] = group

View file

@ -29,7 +29,7 @@ export default class DirectionSession extends BaseSession {
} }
cancel(data: Data): void { cancel(data: Data): void {
const page = getPage(data, this.snapshot.currentPageId) const page = getPage(data)
for (const { id, direction } of this.snapshot.shapes) { for (const { id, direction } of this.snapshot.shapes) {
const shape = page.shapes[id] as RayShape | LineShape const shape = page.shapes[id] as RayShape | LineShape

View file

@ -18,8 +18,8 @@ export default class HandleSession extends BaseSession {
} }
update(data: Data, point: number[], isAligned: boolean): void { update(data: Data, point: number[], isAligned: boolean): void {
const { currentPageId, handleId, initialShape } = this.snapshot const { handleId, initialShape } = this.snapshot
const shape = getPage(data, currentPageId).shapes[initialShape.id] const shape = getPage(data).shapes[initialShape.id]
const delta = vec.vec(this.origin, point) const delta = vec.vec(this.origin, point)
@ -46,8 +46,8 @@ export default class HandleSession extends BaseSession {
} }
cancel(data: Data): void { cancel(data: Data): void {
const { currentPageId, initialShape } = this.snapshot const { initialShape } = this.snapshot
getPage(data, currentPageId).shapes[initialShape.id] = initialShape getPage(data).shapes[initialShape.id] = initialShape
} }
complete(data: Data): void { complete(data: Data): void {

View file

@ -76,8 +76,8 @@ export default class RotateSession extends BaseSession {
} }
cancel(data: Data): void { cancel(data: Data): void {
const { currentPageId, initialShapes } = this.snapshot const { initialShapes } = this.snapshot
const page = getPage(data, currentPageId) const page = getPage(data)
for (const { id, point, rotation } of initialShapes) { for (const { id, point, rotation } of initialShapes) {
const shape = page.shapes[id] const shape = page.shapes[id]

View file

@ -80,9 +80,9 @@ export default class TransformSession extends BaseSession {
} }
cancel(data: Data): void { cancel(data: Data): void {
const { currentPageId, shapeBounds } = this.snapshot const { shapeBounds } = this.snapshot
const { shapes } = getPage(data, currentPageId) const { shapes } = getPage(data)
for (const id in shapeBounds) { for (const id in shapeBounds) {
const shape = shapes[id] const shape = shapes[id]

View file

@ -36,10 +36,9 @@ export default class TransformSingleSession extends BaseSession {
update(data: Data, point: number[], isAspectRatioLocked = false): void { update(data: Data, point: number[], isAspectRatioLocked = false): void {
const { transformType } = this const { transformType } = this
const { initialShapeBounds, currentPageId, initialShape, id } = const { initialShapeBounds, initialShape, id } = this.snapshot
this.snapshot
const shape = getShape(data, id, currentPageId) const shape = getShape(data, id)
const newBoundingBox = getTransformedBoundingBox( const newBoundingBox = getTransformedBoundingBox(
initialShapeBounds, initialShapeBounds,

View file

@ -33,9 +33,8 @@ export default class TranslateSession extends BaseSession {
isAligned: boolean, isAligned: boolean,
isCloning: boolean isCloning: boolean
): void { ): void {
const { currentPageId, clones, initialShapes, initialParents } = const { clones, initialShapes, initialParents } = this.snapshot
this.snapshot const { shapes } = getPage(data)
const { shapes } = getPage(data, currentPageId)
const delta = vec.vec(this.origin, point) const delta = vec.vec(this.origin, point)
@ -143,9 +142,8 @@ export default class TranslateSession extends BaseSession {
} }
cancel(data: Data): void { cancel(data: Data): void {
const { initialShapes, initialParents, clones, currentPageId } = const { initialShapes, initialParents, clones } = this.snapshot
this.snapshot const { shapes } = getPage(data)
const { shapes } = getPage(data, currentPageId)
for (const { id } of initialShapes) { for (const { id } of initialShapes) {
getDocumentBranch(data, id).forEach((id) => { getDocumentBranch(data, id).forEach((id) => {

View file

@ -5,7 +5,8 @@ import {
rng, rng,
getBoundsFromPoints, getBoundsFromPoints,
translateBounds, translateBounds,
pointsBetween, pointInBounds,
pointInCircle,
} from 'utils' } from 'utils'
import { import {
ArrowShape, ArrowShape,
@ -15,12 +16,10 @@ import {
ShapeType, ShapeType,
} from 'types' } from 'types'
import { circleFromThreePoints, isAngleBetween } from 'utils' import { circleFromThreePoints, isAngleBetween } from 'utils'
import { pointInBounds } from 'utils/hitTests'
import { import {
intersectArcBounds, intersectArcBounds,
intersectLineSegmentBounds, intersectLineSegmentBounds,
} from 'utils/intersections' } from 'utils/intersections'
import { pointInCircle } from 'utils/hitTests'
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 React from 'react' import React from 'react'
@ -502,8 +501,8 @@ function renderFreehandArrowShaft(shape: ArrowShape) {
const stroke = getStroke( const stroke = getStroke(
[ [
...pointsBetween(start.point, m), ...vec.pointsBetween(start.point, m),
...pointsBetween(m, end.point), ...vec.pointsBetween(m, end.point),
end.point, end.point,
end.point, end.point,
end.point, end.point,

View file

@ -1,8 +1,7 @@
import { uniqueId } from 'utils' import { uniqueId } from 'utils'
import { DotShape, ShapeType } from 'types' import { DotShape, ShapeType } from 'types'
import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { translateBounds } from 'utils' import { boundsContained, translateBounds } from 'utils'
import { defaultStyle } from 'state/shape-styles' import { defaultStyle } from 'state/shape-styles'
import { registerShapeUtils } from './register' import { registerShapeUtils } from './register'

View file

@ -2,13 +2,13 @@ import { uniqueId } from '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'
import { boundsContain } from 'utils/bounds'
import getStroke, { getStrokePoints } from 'perfect-freehand' import getStroke, { getStrokePoints } from 'perfect-freehand'
import { import {
getBoundsCenter, getBoundsCenter,
getBoundsFromPoints, getBoundsFromPoints,
getSvgPathFromStroke, getSvgPathFromStroke,
translateBounds, translateBounds,
boundsContain,
} from 'utils' } from 'utils'
import { defaultStyle, getShapeStyle } from 'state/shape-styles' import { defaultStyle, getShapeStyle } from 'state/shape-styles'
import { registerShapeUtils } from './register' import { registerShapeUtils } from './register'

View file

@ -2,15 +2,15 @@ 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'
import { boundsContained, getRotatedEllipseBounds } from 'utils/bounds'
import { intersectEllipseBounds } from 'utils/intersections' import { intersectEllipseBounds } from 'utils/intersections'
import { pointInEllipse } from 'utils/hitTests'
import { import {
uniqueId, uniqueId,
ease,
getSvgPathFromStroke, getSvgPathFromStroke,
rng, rng,
translateBounds, translateBounds,
pointInEllipse,
boundsContained,
getRotatedEllipseBounds,
} 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'
@ -207,7 +207,8 @@ function renderPath(shape: EllipseShape) {
} }
for (let i = 5; i < 32; i++) { for (let i = 5; i < 32; i++) {
const rads = start + overlap * 2 + Math.PI * 2.5 * ease(i / 35) const t = i / 35
const rads = start + overlap * 2 + Math.PI * 2.5 * (t * t * t)
const x = rx * Math.cos(rads) + center[0] const x = rx * Math.cos(rads) + center[0]
const y = ry * Math.sin(rads) + center[1] const y = ry * Math.sin(rads) + center[1]
points.push([x, y]) points.push([x, y])

View file

@ -1,10 +1,9 @@
import { uniqueId } from 'utils' import { uniqueId } from 'utils'
import vec from 'utils/vec' import vec from 'utils/vec'
import { LineShape, ShapeType } from 'types' import { LineShape, ShapeType } from 'types'
import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { ThinLine } from 'components/canvas/misc' import { ThinLine } from 'components/canvas/misc'
import { translateBounds } from 'utils' import { translateBounds, boundsContained } from 'utils'
import { defaultStyle } from 'state/shape-styles' import { defaultStyle } from 'state/shape-styles'
import { registerShapeUtils } from './register' import { registerShapeUtils } from './register'

View file

@ -2,8 +2,11 @@ import { uniqueId } from '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'
import { boundsContainPolygon } from 'utils/bounds' import {
import { getBoundsFromPoints, translateBounds } from 'utils' boundsContainPolygon,
getBoundsFromPoints,
translateBounds,
} from 'utils'
import { defaultStyle } from 'state/shape-styles' import { defaultStyle } from 'state/shape-styles'
import { registerShapeUtils } from './register' import { registerShapeUtils } from './register'

View file

@ -1,10 +1,9 @@
import { uniqueId } from 'utils' import { uniqueId } from 'utils'
import vec from 'utils/vec' import vec from 'utils/vec'
import { RayShape, ShapeType } from 'types' import { RayShape, ShapeType } from 'types'
import { boundsContained } from 'utils/bounds'
import { intersectCircleBounds } from 'utils/intersections' import { intersectCircleBounds } from 'utils/intersections'
import { ThinLine } from 'components/canvas/misc' import { ThinLine } from 'components/canvas/misc'
import { translateBounds } from 'utils' import { translateBounds, boundsContained } from 'utils'
import { defaultStyle } from 'state/shape-styles' import { defaultStyle } from 'state/shape-styles'
import { registerShapeUtils } from './register' import { registerShapeUtils } from './register'

View file

@ -1,13 +1,7 @@
import { uniqueId } from 'utils' import { uniqueId } from 'utils'
import vec from 'utils/vec' import vec from 'utils/vec'
import { DashStyle, RectangleShape, ShapeType } from 'types' import { DashStyle, RectangleShape, ShapeType } from 'types'
import { import { getSvgPathFromStroke, translateBounds, rng, shuffleArr } from 'utils'
getSvgPathFromStroke,
translateBounds,
rng,
shuffleArr,
pointsBetween,
} 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'
@ -203,10 +197,10 @@ function renderPath(shape: RectangleShape) {
const lines = shuffleArr( const lines = shuffleArr(
[ [
pointsBetween(tr, br), vec.pointsBetween(tr, br),
pointsBetween(br, bl), vec.pointsBetween(br, bl),
pointsBetween(bl, tl), vec.pointsBetween(bl, tl),
pointsBetween(tl, tr), vec.pointsBetween(tl, tr),
], ],
Math.floor(5 + getRandom() * 4) Math.floor(5 + getRandom() * 4)
) )

View file

@ -1,10 +1,15 @@
import { Shape, ShapeUtility } from 'types' import { Shape, ShapeUtility } from 'types'
import vec from 'utils/vec' import vec from 'utils/vec'
import { getBoundsCenter, getBoundsFromPoints, getRotatedCorners } from 'utils' import {
import { boundsCollidePolygon, boundsContainPolygon } from 'utils/bounds' pointInBounds,
getBoundsCenter,
getBoundsFromPoints,
getRotatedCorners,
boundsCollidePolygon,
boundsContainPolygon,
} from 'utils'
import { uniqueId } from 'utils' import { uniqueId } from 'utils'
import React from 'react' import React from 'react'
import { pointInBounds } from 'utils/hitTests'
function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> { function getDefaultShapeUtil<T extends Shape>(): ShapeUtility<T> {
return { return {

View file

@ -11,7 +11,6 @@ import commands from './commands'
import { import {
getChildren, getChildren,
getCommonBounds, getCommonBounds,
getCurrent,
getCurrentCamera, getCurrentCamera,
getPage, getPage,
getSelectedBounds, getSelectedBounds,
@ -27,8 +26,10 @@ import {
setSelectedIds, setSelectedIds,
getPageState, getPageState,
setToArray, setToArray,
copyToClipboard, deepClone,
pointInBounds,
} from 'utils' } from 'utils'
import { import {
Data, Data,
PointerInfo, PointerInfo,
@ -47,7 +48,6 @@ import {
ColorStyle, ColorStyle,
} from 'types' } from 'types'
import session from './session' import session from './session'
import { pointInBounds } from 'utils/hitTests'
const initialData: Data = { const initialData: Data = {
isReadOnly: false, isReadOnly: false,
@ -335,8 +335,6 @@ const state = createState({
selecting: { selecting: {
onEnter: ['setActiveToolSelect', 'clearInputs'], onEnter: ['setActiveToolSelect', 'clearInputs'],
on: { on: {
UNDO: 'undo',
REDO: 'redo',
SAVED: 'forceSave', SAVED: 'forceSave',
DELETED: { DELETED: {
unless: 'isReadOnly', unless: 'isReadOnly',
@ -773,8 +771,6 @@ const state = createState({
do: 'createShape', do: 'createShape',
to: 'arrow.editing', to: 'arrow.editing',
}, },
UNDO: { do: 'undo' },
REDO: { do: 'redo' },
}, },
}, },
editing: { editing: {
@ -1144,7 +1140,7 @@ const state = createState({
const shape = createShape(type, { const shape = createShape(type, {
parentId: data.currentPageId, parentId: data.currentPageId,
point: vec.round(screenToWorld(payload.point, data)), point: vec.round(screenToWorld(payload.point, data)),
style: getCurrent(data.currentStyle), style: deepClone(data.currentStyle),
}) })
const siblings = getChildren(data, shape.parentId) const siblings = getChildren(data, shape.parentId)
@ -1748,7 +1744,7 @@ const state = createState({
data, data,
payload: { shapes: Shape[]; controls: CodeControl[] } payload: { shapes: Shape[]; controls: CodeControl[] }
) { ) {
commands.generate(data, data.currentPageId, payload.shapes) commands.generate(data, payload.shapes)
}, },
setCodeControls(data, payload: { controls: CodeControl[] }) { setCodeControls(data, payload: { controls: CodeControl[] }) {
data.codeControls = Object.fromEntries( data.codeControls = Object.fromEntries(
@ -1776,7 +1772,7 @@ const state = createState({
data.document.code[data.currentCodeFileId].code data.document.code[data.currentCodeFileId].code
) )
commands.generate(data, data.currentPageId, shapes) commands.generate(data, shapes)
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@ -1807,7 +1803,7 @@ const state = createState({
}, },
copyStateToClipboard(data) { copyStateToClipboard(data) {
copyToClipboard(JSON.stringify(data)) clipboard.copyStringToClipboard(JSON.stringify(data))
}, },
pasteFromClipboard() { pasteFromClipboard() {

View file

@ -1,94 +0,0 @@
import { Bounds } from 'types'
import { pointInBounds } from './hitTests'
import { intersectPolygonBounds } from './intersections'
/**
* Get whether two bounds collide.
* @param a Bounds
* @param b Bounds
* @returns
*/
export function boundsCollide(a: Bounds, b: Bounds): boolean {
return !(
a.maxX < b.minX ||
a.minX > b.maxX ||
a.maxY < b.minY ||
a.minY > b.maxY
)
}
/**
* Get whether the bounds of A contain the bounds of B. A perfect match will return true.
* @param a Bounds
* @param b Bounds
* @returns
*/
export function boundsContain(a: Bounds, b: Bounds): boolean {
return (
a.minX < b.minX && a.minY < b.minY && a.maxY > b.maxY && a.maxX > b.maxX
)
}
/**
* Get whether the bounds of A are contained by the bounds of B.
* @param a Bounds
* @param b Bounds
* @returns
*/
export function boundsContained(a: Bounds, b: Bounds): boolean {
return boundsContain(b, a)
}
/**
* Get whether a set of points are all contained by a bounding box.
* @returns
*/
export function boundsContainPolygon(a: Bounds, points: number[][]): boolean {
return points.every((point) => pointInBounds(point, a))
}
/**
* Get whether a polygon collides a bounding box.
* @param points
* @param b
*/
export function boundsCollidePolygon(a: Bounds, points: number[][]): boolean {
return intersectPolygonBounds(points, a).length > 0
}
/**
* Get whether two bounds are identical.
* @param a Bounds
* @param b Bounds
* @returns
*/
export function boundsAreEqual(a: Bounds, b: Bounds): boolean {
return !(
b.maxX !== a.maxX ||
b.minX !== a.minX ||
b.maxY !== a.maxY ||
b.minY !== a.minY
)
}
export function getRotatedEllipseBounds(
x: number,
y: number,
rx: number,
ry: number,
rotation: number
): Bounds {
const c = Math.cos(rotation)
const s = Math.sin(rotation)
const w = Math.hypot(rx * c, ry * s)
const h = Math.hypot(rx * s, ry * c)
return {
minX: x + rx - w,
minY: y + ry - h,
maxX: x + rx + w,
maxY: y + ry + h,
width: w * 2,
height: h * 2,
}
}

View file

@ -8,15 +8,19 @@ type GTagEvent = {
} }
export const pageview = (url: URL): void => { export const pageview = (url: URL): void => {
;(window as any).gtag('config', GA_TRACKING_ID, { if ('gtag' in window) {
page_path: url, ;(window as any)?.gtag('config', GA_TRACKING_ID, {
}) page_path: url,
})
}
} }
export const event = ({ action, category, label, value }: GTagEvent): void => { export const event = ({ action, category, label, value }: GTagEvent): void => {
;(window as any).gtag('event', action, { if ('gtag' in window) {
event_category: category, ;(window as any)?.gtag('event', action, {
event_label: label, event_category: category,
value: value, event_label: label,
}) value: value,
})
}
} }

View file

@ -1,48 +0,0 @@
import { Bounds } from 'types'
import vec from './vec'
/**
* Get whether a point is inside of a bounds.
* @param A
* @param b
* @returns
*/
export function pointInBounds(A: number[], b: Bounds): boolean {
return !(A[0] < b.minX || A[0] > b.maxX || A[1] < b.minY || A[1] > b.maxY)
}
/**
* Get whether a point is inside of a circle.
* @param A
* @param b
* @returns
*/
export function pointInCircle(A: number[], C: number[], r: number): boolean {
return vec.dist(A, C) <= r
}
/**
* Get whether a point is inside of an ellipse.
* @param point
* @param center
* @param rx
* @param ry
* @param rotation
* @returns
*/
export function pointInEllipse(
A: number[],
C: number[],
rx: number,
ry: number,
rotation = 0
): boolean {
rotation = rotation || 0
const cos = Math.cos(rotation)
const sin = Math.sin(rotation)
const delta = vec.sub(A, C)
const tdx = cos * delta[0] + sin * delta[1]
const tdy = sin * delta[0] - cos * delta[1]
return (tdx * tdx) / (rx * rx) + (tdy * tdy) / (ry * ry) <= 1
}

View file

@ -12,6 +12,31 @@ function getIntersection(message: string, ...points: number[][]) {
return { didIntersect: points.length > 0, message, points } return { didIntersect: points.length > 0, message, points }
} }
export function intersectRays(
p0: number[],
n0: number[],
p1: number[],
n1: number[]
): Intersection {
const dx = p1[0] - p0[0]
const dy = p1[1] - p0[1]
const det = n1[0] * n0[1] - n1[1] * n0[0]
const u = (dy * n1[0] - dx * n1[1]) / det
const v = (dy * n0[0] - dx * n0[1]) / det
if (u < 0 || v < 0) return getIntersection('miss')
const m0 = n0[1] / n0[0]
const m1 = n1[1] / n1[0]
const b0 = p0[1] - m0 * p0[0]
const b1 = p1[1] - m1 * p1[0]
const x = (b1 - b0) / (m0 - m1)
const y = m0 * x + b0
return Number.isFinite(x)
? getIntersection('intersection', [x, y])
: getIntersection('parallel')
}
export function intersectLineSegments( export function intersectLineSegments(
a1: number[], a1: number[],
a2: number[], a2: number[],
@ -45,6 +70,27 @@ export function intersectLineSegments(
return getIntersection('no intersection') return getIntersection('no intersection')
} }
export function intersectCircleCircle(a: number[], b: number[]): Intersection {
const R = a[2],
r = b[2]
let dx = b[0] - a[0],
dy = b[1] - a[1]
const d = Math.sqrt(dx * dx + dy * dy),
x = (d * d - r * r + R * R) / (2 * d),
y = Math.sqrt(R * R - x * x)
dx /= d
dy /= d
return getIntersection(
'intersection',
[a[0] + dx * x - dy * y, a[1] + dy * x + dx * y],
[a[0] + dx * x + dy * y, a[1] + dy * x - dx * y]
)
}
export function intersectCircleLineSegment( export function intersectCircleLineSegment(
c: number[], c: number[],
r: number, r: number,

File diff suppressed because it is too large Load diff

View file

@ -472,7 +472,7 @@ export default class Vec {
} }
/** /**
* Get a vector d distance from A towards B. * Push a point A towards point B by a given distance.
* @param A * @param A
* @param B * @param B
* @param d * @param d
@ -482,6 +482,16 @@ export default class Vec {
return Vec.add(A, Vec.mul(Vec.uni(Vec.vec(A, B)), d)) return Vec.add(A, Vec.mul(Vec.uni(Vec.vec(A, B)), d))
} }
/**
* Push a point in a given angle by a given distance.
* @param A
* @param B
* @param d
*/
static nudgeAtAngle = (A: number[], a: number, d: number): number[] => {
return [Math.cos(a) * d + A[0], Math.sin(a) * d + A[1]]
}
/** /**
* Round a vector to a precision length. * Round a vector to a precision length.
* @param a * @param a
@ -490,4 +500,19 @@ export default class Vec {
static toPrecision = (a: number[], n = 4): number[] => { static toPrecision = (a: number[], n = 4): number[] => {
return [+a[0].toPrecision(n), +a[1].toPrecision(n)] return [+a[0].toPrecision(n), +a[1].toPrecision(n)]
} }
/**
* Get a number of points between two points.
* @param a
* @param b
* @param steps
*/
static pointsBetween = (a: number[], b: number[], steps = 6): number[][] => {
return Array.from(Array(steps))
.map((_, i) => {
const t = i / steps
return t * t * t
})
.map((t) => [...Vec.lrp(a, b, t), (1 - t) / 2])
}
} }